Skip to main content

clawft_kernel/
console.rs

1//! Kernel console: boot event types and output formatting.
2//!
3//! Provides [`BootEvent`], [`BootPhase`], and [`LogLevel`] types for
4//! recording and displaying kernel boot output. The interactive REPL
5//! loop is stubbed (requires complex stdin handling); only the event
6//! types and output formatting are implemented.
7
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::mpsc::{self, Receiver, SyncSender};
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15/// Phase of the boot sequence.
16#[non_exhaustive]
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum BootPhase {
19    /// Pre-boot initialization.
20    Init,
21    /// Loading configuration.
22    Config,
23    /// Registering system services.
24    Services,
25    /// Loading resource tree from DAG.
26    ResourceTree,
27    /// Spawning service agents.
28    Agents,
29    /// Network service discovery.
30    Network,
31    /// ECC cognitive substrate initialization.
32    Ecc,
33    /// Boot complete, ready for commands.
34    Ready,
35}
36
37impl BootPhase {
38    /// Short tag string for console output (e.g. `[INIT]`).
39    pub fn tag(&self) -> &'static str {
40        match self {
41            BootPhase::Init => "INIT",
42            BootPhase::Config => "CONFIG",
43            BootPhase::Services => "SERVICES",
44            BootPhase::ResourceTree => "TREE",
45            BootPhase::Agents => "AGENTS",
46            BootPhase::Network => "NETWORK",
47            BootPhase::Ecc => "ECC",
48            BootPhase::Ready => "READY",
49        }
50    }
51}
52
53impl std::fmt::Display for BootPhase {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "{}", self.tag())
56    }
57}
58
59/// Log level for boot events.
60#[non_exhaustive]
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub enum LogLevel {
63    /// Debug-level messages (not shown in normal boot output).
64    Debug,
65    /// Informational messages (standard boot output).
66    Info,
67    /// Warning messages.
68    Warn,
69    /// Error messages.
70    Error,
71}
72
73/// A single boot event recorded during kernel startup.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct BootEvent {
76    /// When the event occurred.
77    pub timestamp: DateTime<Utc>,
78    /// Which boot phase generated the event.
79    pub phase: BootPhase,
80    /// Human-readable event message.
81    pub message: String,
82    /// Severity level.
83    pub level: LogLevel,
84    /// Monotonic sequence assigned by [`KernelEventLog`] on ingest
85    /// (WEFT-434). Zero for events that never passed through the
86    /// event log (or legacy payloads that predate the field).
87    #[serde(default)]
88    pub seq: u64,
89}
90
91impl BootEvent {
92    /// Create a new info-level boot event.
93    pub fn info(phase: BootPhase, message: impl Into<String>) -> Self {
94        Self {
95            timestamp: Utc::now(),
96            phase,
97            message: message.into(),
98            level: LogLevel::Info,
99            seq: 0,
100        }
101    }
102
103    /// Create a new warning-level boot event.
104    pub fn warn(phase: BootPhase, message: impl Into<String>) -> Self {
105        Self {
106            timestamp: Utc::now(),
107            phase,
108            message: message.into(),
109            level: LogLevel::Warn,
110            seq: 0,
111        }
112    }
113
114    /// Create a new error-level boot event.
115    pub fn error(phase: BootPhase, message: impl Into<String>) -> Self {
116        Self {
117            timestamp: Utc::now(),
118            phase,
119            message: message.into(),
120            level: LogLevel::Error,
121            seq: 0,
122        }
123    }
124
125    /// Format this event for console display.
126    ///
127    /// Example: `  [INIT]      WeftOS v0.6.19 booting...` (version
128    /// pulled from `CARGO_PKG_VERSION`).
129    pub fn format_line(&self) -> String {
130        let tag = self.phase.tag();
131        format!("  [{tag:<10}] {}", self.message)
132    }
133}
134
135/// Boot log: a recorded sequence of boot events.
136///
137/// Used to replay boot output when attaching to a running kernel
138/// or for diagnostics.
139#[derive(Debug, Clone, Default)]
140pub struct BootLog {
141    events: Vec<BootEvent>,
142}
143
144impl BootLog {
145    /// Create an empty boot log.
146    pub fn new() -> Self {
147        Self { events: Vec::new() }
148    }
149
150    /// Record a boot event.
151    pub fn push(&mut self, event: BootEvent) {
152        self.events.push(event);
153    }
154
155    /// Get all recorded events.
156    pub fn events(&self) -> &[BootEvent] {
157        &self.events
158    }
159
160    /// Format all events for console display.
161    pub fn format_all(&self) -> String {
162        let mut output = String::new();
163        for event in &self.events {
164            if event.level == LogLevel::Debug {
165                continue;
166            }
167            output.push_str(&event.format_line());
168            output.push('\n');
169        }
170        output
171    }
172
173    /// Get the number of recorded events.
174    pub fn len(&self) -> usize {
175        self.events.len()
176    }
177
178    /// Check whether the log is empty.
179    pub fn is_empty(&self) -> bool {
180        self.events.is_empty()
181    }
182}
183
184/// Default capacity of the kernel event ring buffer.
185const DEFAULT_EVENT_LOG_CAPACITY: usize = 1024;
186
187/// Default capacity of a live-log subscriber channel (WEFT-434).
188const EVENT_LOG_SUBSCRIBER_CAPACITY: usize = 1024;
189
190/// Thread-safe ring buffer for kernel runtime events.
191///
192/// Captures boot events and any post-boot events (service starts/stops,
193/// agent spawns, health checks, errors). Holds at most `capacity` events
194/// — when full, the oldest event is evicted.
195///
196/// Used by the daemon to serve `kernel.logs` / `kernel.logs_stream` RPC
197/// requests. Live subscribers receive entries as they are ingested
198/// (WEFT-434); each entry carries a monotonic [`BootEvent::seq`].
199pub struct KernelEventLog {
200    events: std::sync::Mutex<std::collections::VecDeque<BootEvent>>,
201    capacity: usize,
202    /// Next sequence number to assign (starts at 1 so 0 stays "unset").
203    next_seq: AtomicU64,
204    /// Live stream subscribers keyed by subscription id.
205    ///
206    /// Uses `std::sync::mpsc` (not tokio) so the event log compiles for
207    /// wasm32 `--no-default-features` where tokio is not linked. The
208    /// daemon stream bridge polls with `try_recv` on the async runtime.
209    subscribers: std::sync::Mutex<HashMap<u64, SyncSender<BootEvent>>>,
210    next_sub_id: AtomicU64,
211}
212
213impl KernelEventLog {
214    /// Create a new event log with the default capacity (1024).
215    pub fn new() -> Self {
216        Self::with_capacity(DEFAULT_EVENT_LOG_CAPACITY)
217    }
218
219    /// Create a new event log with a specific capacity.
220    pub fn with_capacity(capacity: usize) -> Self {
221        Self {
222            events: std::sync::Mutex::new(std::collections::VecDeque::with_capacity(capacity)),
223            capacity,
224            next_seq: AtomicU64::new(1),
225            subscribers: std::sync::Mutex::new(HashMap::new()),
226            next_sub_id: AtomicU64::new(1),
227        }
228    }
229
230    /// Push an event into the ring buffer and fan out to live subscribers.
231    ///
232    /// Assigns a monotonic [`BootEvent::seq`] (starting at 1). The write
233    /// lock is released before subscriber notification so a slow
234    /// consumer cannot block ingest.
235    pub fn push(&self, mut event: BootEvent) {
236        let seq = self.next_seq.fetch_add(1, Ordering::Relaxed);
237        event.seq = seq;
238        {
239            let mut events = self.events.lock().unwrap();
240            if events.len() >= self.capacity {
241                events.pop_front();
242            }
243            events.push_back(event.clone());
244        }
245
246        // Fan out (non-blocking). Drop dead subscribers so a closed
247        // stream RPC cannot accumulate senders forever.
248        let mut dead = Vec::new();
249        {
250            let subs = self.subscribers.lock().unwrap();
251            for (id, tx) in subs.iter() {
252                if tx.try_send(event.clone()).is_err() {
253                    dead.push(*id);
254                }
255            }
256        }
257        if !dead.is_empty() {
258            let mut subs = self.subscribers.lock().unwrap();
259            for id in dead {
260                subs.remove(&id);
261            }
262        }
263    }
264
265    /// Push a simple info event with a source tag and message.
266    pub fn info(&self, source: &str, message: impl Into<String>) {
267        self.push(BootEvent {
268            timestamp: Utc::now(),
269            phase: BootPhase::Ready, // post-boot events use Ready phase
270            message: format!("[{source}] {}", message.into()),
271            level: LogLevel::Info,
272            seq: 0,
273        });
274    }
275
276    /// Push a warning event.
277    pub fn warn(&self, source: &str, message: impl Into<String>) {
278        self.push(BootEvent {
279            timestamp: Utc::now(),
280            phase: BootPhase::Ready,
281            message: format!("[{source}] {}", message.into()),
282            level: LogLevel::Warn,
283            seq: 0,
284        });
285    }
286
287    /// Push an error event.
288    pub fn error(&self, source: &str, message: impl Into<String>) {
289        self.push(BootEvent {
290            timestamp: Utc::now(),
291            phase: BootPhase::Ready,
292            message: format!("[{source}] {}", message.into()),
293            level: LogLevel::Error,
294            seq: 0,
295        });
296    }
297
298    /// Subscribe to real-time log entries (WEFT-434).
299    ///
300    /// Returns a subscription id (for [`Self::unsubscribe`]) and a
301    /// receiver that yields every event ingested after the subscribe
302    /// call. The channel is bounded; a slow consumer is dropped from
303    /// the fan-out set on the next failed `try_send`.
304    pub fn subscribe(&self) -> (u64, Receiver<BootEvent>) {
305        let id = self.next_sub_id.fetch_add(1, Ordering::Relaxed);
306        let (tx, rx) = mpsc::sync_channel(EVENT_LOG_SUBSCRIBER_CAPACITY);
307        self.subscribers.lock().unwrap().insert(id, tx);
308        (id, rx)
309    }
310
311    /// Drop a live subscriber. Idempotent.
312    pub fn unsubscribe(&self, id: u64) {
313        self.subscribers.lock().unwrap().remove(&id);
314    }
315
316    /// Number of active live subscribers.
317    pub fn subscriber_count(&self) -> usize {
318        self.subscribers.lock().unwrap().len()
319    }
320
321    /// Highest sequence number assigned so far (0 if none pushed).
322    pub fn last_seq(&self) -> u64 {
323        self.next_seq.load(Ordering::Relaxed).saturating_sub(1)
324    }
325
326    /// Push an info event and optionally append to the local chain.
327    #[cfg(feature = "exochain")]
328    pub fn info_with_chain(
329        &self,
330        source: &str,
331        message: impl Into<String>,
332        chain: Option<&crate::chain::ChainManager>,
333    ) {
334        let msg = message.into();
335        self.info(source, &msg);
336        if let Some(cm) = chain {
337            cm.append(
338                source,
339                "log.info",
340                Some(serde_json::json!({ "message": msg })),
341            );
342        }
343    }
344
345    /// Ingest all events from a BootLog (used to seed boot events).
346    pub fn ingest_boot_log(&self, boot_log: &BootLog) {
347        for event in boot_log.events() {
348            self.push(event.clone());
349        }
350    }
351
352    /// Get the last `n` events (or all if `n` is 0 or exceeds count).
353    pub fn tail(&self, n: usize) -> Vec<BootEvent> {
354        let events = self.events.lock().unwrap();
355        if n == 0 || n >= events.len() {
356            events.iter().cloned().collect()
357        } else {
358            events.iter().skip(events.len() - n).cloned().collect()
359        }
360    }
361
362    /// Get all events matching a minimum log level.
363    pub fn filter_level(&self, min_level: &LogLevel, n: usize) -> Vec<BootEvent> {
364        let events = self.events.lock().unwrap();
365        let level_rank = |l: &LogLevel| -> u8 {
366            match l {
367                LogLevel::Debug => 0,
368                LogLevel::Info => 1,
369                LogLevel::Warn => 2,
370                LogLevel::Error => 3,
371            }
372        };
373        let min_rank = level_rank(min_level);
374        let filtered: Vec<BootEvent> = events
375            .iter()
376            .filter(|e| level_rank(&e.level) >= min_rank)
377            .cloned()
378            .collect();
379        if n == 0 || n >= filtered.len() {
380            filtered
381        } else {
382            filtered[filtered.len() - n..].to_vec()
383        }
384    }
385
386    /// Current number of events in the buffer.
387    pub fn len(&self) -> usize {
388        self.events.lock().unwrap().len()
389    }
390
391    /// Check if the buffer is empty.
392    pub fn is_empty(&self) -> bool {
393        self.events.lock().unwrap().is_empty()
394    }
395}
396
397impl Default for KernelEventLog {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403impl std::fmt::Debug for KernelEventLog {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        let len = self.len();
406        f.debug_struct("KernelEventLog")
407            .field("count", &len)
408            .field("capacity", &self.capacity)
409            .finish()
410    }
411}
412
413/// Format the boot banner header.
414///
415/// On platforms without OS sandbox support (macOS, etc.), appends a clear
416/// WARN line so operators know `OsSandbox`/`Combined` policies are reduced
417/// to WASM-only isolation (WEFT-70).
418///
419/// Brand text comes from [`clawft_types::config::brand`] (WEFT-176).
420pub fn boot_banner() -> String {
421    let brand = clawft_types::config::brand();
422    let mut output = String::new();
423    output.push_str("\n  ");
424    output.push_str(&brand);
425    output.push_str(" v");
426    output.push_str(env!("CARGO_PKG_VERSION"));
427    output.push('\n');
428    output.push_str("  ");
429    output.push_str(&"-".repeat(45));
430    output.push('\n');
431    if let Some(msg) = clawft_plugin::sandbox::sandbox_platform_banner_warning() {
432        output.push_str("  WARN  ");
433        output.push_str(msg);
434        output.push('\n');
435    }
436    output
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn boot_phase_tags() {
445        assert_eq!(BootPhase::Init.tag(), "INIT");
446        assert_eq!(BootPhase::Config.tag(), "CONFIG");
447        assert_eq!(BootPhase::Services.tag(), "SERVICES");
448        assert_eq!(BootPhase::ResourceTree.tag(), "TREE");
449        assert_eq!(BootPhase::Agents.tag(), "AGENTS");
450        assert_eq!(BootPhase::Network.tag(), "NETWORK");
451        assert_eq!(BootPhase::Ready.tag(), "READY");
452    }
453
454    #[test]
455    fn boot_event_info() {
456        let msg = format!("WeftOS v{} booting...", env!("CARGO_PKG_VERSION"));
457        let event = BootEvent::info(BootPhase::Init, msg.clone());
458        assert_eq!(event.phase, BootPhase::Init);
459        assert_eq!(event.level, LogLevel::Info);
460        assert_eq!(event.message, msg);
461    }
462
463    #[test]
464    fn boot_event_format_line() {
465        let event = BootEvent::info(BootPhase::Init, "PID 0 (kernel)");
466        let line = event.format_line();
467        assert!(line.contains("[INIT"));
468        assert!(line.contains("PID 0 (kernel)"));
469    }
470
471    #[test]
472    fn boot_log_push_and_format() {
473        let mut log = BootLog::new();
474        log.push(BootEvent::info(BootPhase::Init, "booting..."));
475        log.push(BootEvent::info(BootPhase::Config, "config loaded"));
476        log.push(BootEvent::info(BootPhase::Ready, "ready"));
477
478        assert_eq!(log.len(), 3);
479        let formatted = log.format_all();
480        assert!(formatted.contains("booting..."));
481        assert!(formatted.contains("config loaded"));
482        assert!(formatted.contains("ready"));
483    }
484
485    #[test]
486    fn boot_log_skips_debug() {
487        let mut log = BootLog::new();
488        log.push(BootEvent {
489            timestamp: Utc::now(),
490            phase: BootPhase::Init,
491            message: "debug msg".into(),
492            level: LogLevel::Debug,
493            seq: 0,
494        });
495        log.push(BootEvent::info(BootPhase::Init, "info msg"));
496
497        let formatted = log.format_all();
498        assert!(!formatted.contains("debug msg"));
499        assert!(formatted.contains("info msg"));
500    }
501
502    #[test]
503    fn boot_log_empty() {
504        let log = BootLog::new();
505        assert!(log.is_empty());
506        assert_eq!(log.len(), 0);
507        assert!(log.format_all().is_empty());
508    }
509
510    #[test]
511    fn boot_banner_format() {
512        let banner = boot_banner();
513        assert!(banner.contains("WeftOS v"));
514        assert!(banner.contains(env!("CARGO_PKG_VERSION")));
515        assert!(banner.contains("---"));
516    }
517
518    /// WEFT-70: non-Linux hosts surface an OS-sandbox downgrade WARN in the
519    /// startup banner; Linux keeps the banner clean.
520    #[test]
521    fn boot_banner_includes_macos_sandbox_downgrade_warning() {
522        let banner = boot_banner();
523        if cfg!(target_os = "linux") {
524            assert!(
525                !banner.contains("WARN"),
526                "Linux OS sandbox is available; banner must not warn"
527            );
528        } else {
529            assert!(
530                banner.contains("WARN"),
531                "non-Linux banner must include WARN: {banner}"
532            );
533            assert!(
534                banner.contains("OS sandbox unavailable"),
535                "banner must name the sandbox restriction: {banner}"
536            );
537            assert!(
538                banner.contains("WASM"),
539                "banner must name the WASM fallback: {banner}"
540            );
541            if cfg!(target_os = "macos") {
542                assert!(
543                    banner.contains("macOS"),
544                    "macOS banner should call out the platform: {banner}"
545                );
546            }
547        }
548    }
549
550    #[test]
551    fn boot_event_serde() {
552        let event = BootEvent::info(BootPhase::Services, "[OK] message-bus");
553        let json = serde_json::to_string(&event).unwrap();
554        let restored: BootEvent = serde_json::from_str(&json).unwrap();
555        assert_eq!(restored.phase, BootPhase::Services);
556        assert_eq!(restored.message, "[OK] message-bus");
557    }
558
559    #[test]
560    fn boot_event_warn_and_error() {
561        let warn_event = BootEvent::warn(BootPhase::Services, "slow start");
562        assert_eq!(warn_event.level, LogLevel::Warn);
563
564        let err_event = BootEvent::error(BootPhase::Services, "failed");
565        assert_eq!(err_event.level, LogLevel::Error);
566    }
567
568    // ── KernelEventLog tests ──────────────────────────────────
569
570    #[test]
571    fn event_log_push_and_tail() {
572        let log = KernelEventLog::new();
573        log.info("test", "first");
574        log.info("test", "second");
575        log.warn("test", "third");
576
577        assert_eq!(log.len(), 3);
578
579        let all = log.tail(0);
580        assert_eq!(all.len(), 3);
581        assert!(all[0].message.contains("first"));
582
583        let last_two = log.tail(2);
584        assert_eq!(last_two.len(), 2);
585        assert!(last_two[0].message.contains("second"));
586    }
587
588    #[test]
589    fn event_log_ring_buffer_evicts() {
590        let log = KernelEventLog::with_capacity(3);
591        log.info("a", "1");
592        log.info("b", "2");
593        log.info("c", "3");
594        log.info("d", "4"); // evicts "1"
595
596        assert_eq!(log.len(), 3);
597        let all = log.tail(0);
598        assert!(all[0].message.contains("[b] 2"));
599        assert!(all[2].message.contains("[d] 4"));
600    }
601
602    #[test]
603    fn event_log_filter_level() {
604        let log = KernelEventLog::new();
605        log.info("test", "info msg");
606        log.warn("test", "warn msg");
607        log.error("test", "error msg");
608
609        let warns_and_above = log.filter_level(&LogLevel::Warn, 0);
610        assert_eq!(warns_and_above.len(), 2);
611
612        let errors_only = log.filter_level(&LogLevel::Error, 0);
613        assert_eq!(errors_only.len(), 1);
614    }
615
616    #[test]
617    fn event_log_ingest_boot_log() {
618        let mut boot_log = BootLog::new();
619        boot_log.push(BootEvent::info(BootPhase::Init, "booting"));
620        boot_log.push(BootEvent::info(BootPhase::Ready, "ready"));
621
622        let event_log = KernelEventLog::new();
623        event_log.ingest_boot_log(&boot_log);
624
625        assert_eq!(event_log.len(), 2);
626        let events = event_log.tail(0);
627        assert!(events[0].message.contains("booting"));
628    }
629
630    #[test]
631    fn event_log_empty() {
632        let log = KernelEventLog::new();
633        assert!(log.is_empty());
634        assert_eq!(log.len(), 0);
635        assert!(log.tail(10).is_empty());
636    }
637
638    #[test]
639    fn event_log_assigns_monotonic_seq() {
640        let log = KernelEventLog::new();
641        log.info("test", "a");
642        log.info("test", "b");
643        log.warn("test", "c");
644        let all = log.tail(0);
645        assert_eq!(all[0].seq, 1);
646        assert_eq!(all[1].seq, 2);
647        assert_eq!(all[2].seq, 3);
648        assert_eq!(log.last_seq(), 3);
649    }
650
651    #[test]
652    fn event_log_subscribe_receives_new_entries() {
653        let log = KernelEventLog::new();
654        log.info("test", "before-sub");
655        let (id, rx) = log.subscribe();
656        assert_eq!(log.subscriber_count(), 1);
657        log.info("test", "after-sub");
658        let got = rx.recv_timeout(std::time::Duration::from_secs(1)).expect("entry");
659        assert!(got.message.contains("after-sub"));
660        assert!(got.seq >= 2);
661        log.unsubscribe(id);
662        assert_eq!(log.subscriber_count(), 0);
663    }
664}