Skip to main content

clawft_types/config/
kernel.rs

1//! Kernel configuration types.
2//!
3//! These types are defined in `clawft-types` so they can be embedded
4//! in the root [`Config`](super::Config) without creating a circular
5//! dependency with `clawft-kernel`.
6
7use serde::{Deserialize, Serialize};
8
9/// Default maximum number of concurrent processes.
10fn default_max_processes() -> u32 {
11    64
12}
13
14/// Default health check interval in seconds.
15fn default_health_check_interval_secs() -> u64 {
16    30
17}
18
19/// Kernel is enabled by default.
20fn default_enabled() -> bool {
21    true
22}
23
24/// Cluster networking configuration for distributed WeftOS nodes.
25///
26/// Controls the ruvector-powered clustering layer that coordinates
27/// native nodes. Browser/edge nodes join via WebSocket to a
28/// coordinator and do not need this configuration.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ClusterNetworkConfig {
31    /// Number of replica copies for each shard (default: 3).
32    #[serde(default = "default_replication_factor", alias = "replicationFactor")]
33    pub replication_factor: usize,
34
35    /// Total number of shards in the cluster (default: 64).
36    #[serde(default = "default_shard_count", alias = "shardCount")]
37    pub shard_count: u32,
38
39    /// Interval between heartbeat checks in seconds (default: 5).
40    #[serde(
41        default = "default_cluster_heartbeat",
42        alias = "heartbeatIntervalSecs"
43    )]
44    pub heartbeat_interval_secs: u64,
45
46    /// Timeout before marking a node offline in seconds (default: 30).
47    #[serde(default = "default_node_timeout", alias = "nodeTimeoutSecs")]
48    pub node_timeout_secs: u64,
49
50    /// Whether to enable DAG-based consensus (default: true).
51    #[serde(default = "default_enable_consensus", alias = "enableConsensus")]
52    pub enable_consensus: bool,
53
54    /// Minimum nodes required for quorum (default: 2).
55    #[serde(default = "default_min_quorum", alias = "minQuorumSize")]
56    pub min_quorum_size: usize,
57
58    /// Seed node addresses for discovery (coordinator addresses).
59    #[serde(default, alias = "seedNodes")]
60    pub seed_nodes: Vec<String>,
61
62    /// Human-readable display name for this node.
63    #[serde(default, alias = "nodeName")]
64    pub node_name: Option<String>,
65}
66
67fn default_replication_factor() -> usize {
68    3
69}
70fn default_shard_count() -> u32 {
71    64
72}
73fn default_cluster_heartbeat() -> u64 {
74    5
75}
76fn default_node_timeout() -> u64 {
77    30
78}
79fn default_enable_consensus() -> bool {
80    true
81}
82fn default_min_quorum() -> usize {
83    2
84}
85
86impl Default for ClusterNetworkConfig {
87    fn default() -> Self {
88        Self {
89            replication_factor: default_replication_factor(),
90            shard_count: default_shard_count(),
91            heartbeat_interval_secs: default_cluster_heartbeat(),
92            node_timeout_secs: default_node_timeout(),
93            enable_consensus: default_enable_consensus(),
94            min_quorum_size: default_min_quorum(),
95            seed_nodes: Vec::new(),
96            node_name: None,
97        }
98    }
99}
100
101/// Kernel subsystem configuration.
102///
103/// Embedded in the root `Config` under the `kernel` key. All fields
104/// have sensible defaults so that existing configuration files parse
105/// without errors.
106///
107/// # Example JSON
108///
109/// ```json
110/// {
111///   "kernel": {
112///     "enabled": false,
113///     "max_processes": 128,
114///     "health_check_interval_secs": 15
115///   }
116/// }
117/// ```
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct KernelConfig {
120    /// Whether the kernel subsystem is enabled.
121    ///
122    /// When `false`, kernel subsystems do not activate unless explicitly
123    /// invoked via `weave kernel` CLI commands. Defaults to `true`.
124    #[serde(default = "default_enabled")]
125    pub enabled: bool,
126
127    /// Maximum number of concurrent processes in the process table.
128    #[serde(default = "default_max_processes", alias = "maxProcesses")]
129    pub max_processes: u32,
130
131    /// Interval (in seconds) between periodic health checks.
132    #[serde(
133        default = "default_health_check_interval_secs",
134        alias = "healthCheckIntervalSecs"
135    )]
136    pub health_check_interval_secs: u64,
137
138    /// Cluster networking configuration (native coordinator nodes).
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub cluster: Option<ClusterNetworkConfig>,
141
142    /// Local chain configuration (exochain feature).
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub chain: Option<ChainConfig>,
145
146    /// Resource tree configuration (exochain feature).
147    #[serde(default, skip_serializing_if = "Option::is_none", alias = "resourceTree")]
148    pub resource_tree: Option<ResourceTreeConfig>,
149
150    /// Vector search backend configuration (ECC feature).
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub vector: Option<VectorConfig>,
153
154    /// Per-user profile namespace configuration.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub profiles: Option<ProfilesConfig>,
157
158    /// Time-windowed pairing configuration.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub pairing: Option<PairingConfig>,
161}
162
163impl Default for KernelConfig {
164    fn default() -> Self {
165        Self {
166            enabled: true,
167            max_processes: default_max_processes(),
168            health_check_interval_secs: default_health_check_interval_secs(),
169            cluster: None,
170            chain: None,
171            resource_tree: None,
172            vector: None,
173            profiles: None,
174            pairing: None,
175        }
176    }
177}
178
179// ── Profile namespace configuration ─────────────────────────────────────
180
181/// Per-user profile namespace configuration.
182///
183/// When enabled, each profile gets its own isolated vector storage
184/// directory under `storage_path`.
185///
186/// # Example TOML
187///
188/// ```toml
189/// [kernel.profiles]
190/// enabled = true
191/// storage_path = ".weftos/profiles"
192/// default_profile = "default"
193/// ```
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ProfilesConfig {
196    /// Whether profile namespaces are enabled.
197    #[serde(default = "default_profiles_enabled")]
198    pub enabled: bool,
199
200    /// Base directory for profile data.
201    #[serde(default = "default_profiles_storage_path")]
202    pub storage_path: String,
203
204    /// Default profile to activate on boot.
205    #[serde(default = "default_profile_name")]
206    pub default_profile: String,
207}
208
209fn default_profiles_enabled() -> bool {
210    true
211}
212
213fn default_profiles_storage_path() -> String {
214    ".weftos/profiles".to_owned()
215}
216
217fn default_profile_name() -> String {
218    "default".to_owned()
219}
220
221impl Default for ProfilesConfig {
222    fn default() -> Self {
223        Self {
224            enabled: default_profiles_enabled(),
225            storage_path: default_profiles_storage_path(),
226            default_profile: default_profile_name(),
227        }
228    }
229}
230
231// ── Time-windowed pairing configuration ─────────────────────────────────
232
233/// Configuration for time-windowed mesh pairing.
234///
235/// Controls where paired host data is persisted and the default
236/// enrollment window duration.
237///
238/// # Example TOML
239///
240/// ```toml
241/// [kernel.pairing]
242/// persist_path = ".weftos/runtime/paired_hosts.json"
243/// default_window_secs = 30
244/// ```
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct PairingConfig {
247    /// Path to the paired hosts persistence file.
248    #[serde(default = "default_pairing_persist_path")]
249    pub persist_path: String,
250
251    /// Default enrollment window duration in seconds.
252    #[serde(default = "default_pairing_window_secs")]
253    pub default_window_secs: u64,
254}
255
256fn default_pairing_persist_path() -> String {
257    ".weftos/runtime/paired_hosts.json".to_owned()
258}
259
260fn default_pairing_window_secs() -> u64 {
261    30
262}
263
264impl Default for PairingConfig {
265    fn default() -> Self {
266        Self {
267            persist_path: default_pairing_persist_path(),
268            default_window_secs: default_pairing_window_secs(),
269        }
270    }
271}
272
273/// Local chain configuration.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct ChainConfig {
276    /// Whether the local chain is enabled.
277    #[serde(default = "default_true")]
278    pub enabled: bool,
279
280    /// Maximum events before auto-checkpoint.
281    #[serde(default = "default_checkpoint_interval", alias = "checkpointInterval")]
282    pub checkpoint_interval: u64,
283
284    /// Chain ID (0 = local node chain).
285    #[serde(default)]
286    pub chain_id: u32,
287
288    /// Path to the chain checkpoint file for persistence across restarts.
289    /// If `None`, defaults to `~/.clawft/chain/local.json`.
290    #[serde(
291        default,
292        skip_serializing_if = "Option::is_none",
293        alias = "checkpointPath"
294    )]
295    pub checkpoint_path: Option<String>,
296}
297
298fn default_true() -> bool {
299    true
300}
301fn default_checkpoint_interval() -> u64 {
302    1000
303}
304
305impl Default for ChainConfig {
306    fn default() -> Self {
307        Self {
308            enabled: true,
309            checkpoint_interval: default_checkpoint_interval(),
310            chain_id: 0,
311            checkpoint_path: None,
312        }
313    }
314}
315
316impl ChainConfig {
317    /// Returns the effective checkpoint path.
318    ///
319    /// If `checkpoint_path` is set, returns it. Otherwise falls back to
320    /// `~/.clawft/chain.json` (requires the `native` feature for `dirs`).
321    pub fn effective_checkpoint_path(&self) -> Option<String> {
322        if self.checkpoint_path.is_some() {
323            return self.checkpoint_path.clone();
324        }
325        #[cfg(feature = "native")]
326        {
327            dirs::home_dir().map(|h| {
328                h.join(".clawft")
329                    .join("chain.json")
330                    .to_string_lossy()
331                    .into_owned()
332            })
333        }
334        #[cfg(not(feature = "native"))]
335        {
336            None
337        }
338    }
339}
340
341/// Resource tree configuration.
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct ResourceTreeConfig {
344    /// Whether the resource tree is enabled.
345    #[serde(default = "default_true_rt")]
346    pub enabled: bool,
347
348    /// Path to checkpoint file (None = in-memory only).
349    #[serde(
350        default,
351        skip_serializing_if = "Option::is_none",
352        alias = "checkpointPath"
353    )]
354    pub checkpoint_path: Option<String>,
355}
356
357fn default_true_rt() -> bool {
358    true
359}
360
361impl Default for ResourceTreeConfig {
362    fn default() -> Self {
363        Self {
364            enabled: true,
365            checkpoint_path: None,
366        }
367    }
368}
369
370// ── Vector search backend configuration ──────────────────────────────────
371
372/// Which vector search backend to use.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
374#[serde(rename_all = "lowercase")]
375pub enum VectorBackendKind {
376    /// In-memory HNSW (default, fast, suitable for <1M vectors).
377    #[default]
378    Hnsw,
379    /// SSD-backed DiskANN (large scale, 1M+ vectors).
380    DiskAnn,
381    /// Hot HNSW cache + cold DiskANN store.
382    Hybrid,
383}
384
385/// HNSW-specific vector configuration.
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct VectorHnswConfig {
388    /// ef_construction parameter for index building.
389    #[serde(default = "default_ef_construction")]
390    pub ef_construction: usize,
391
392    /// Number of bi-directional links per node (M parameter).
393    #[serde(default = "default_m")]
394    pub m: usize,
395
396    /// Maximum number of elements the index can hold.
397    #[serde(default = "default_max_elements")]
398    pub max_elements: usize,
399}
400
401fn default_ef_construction() -> usize {
402    200
403}
404fn default_m() -> usize {
405    16
406}
407fn default_max_elements() -> usize {
408    100_000
409}
410
411impl Default for VectorHnswConfig {
412    fn default() -> Self {
413        Self {
414            ef_construction: default_ef_construction(),
415            m: default_m(),
416            max_elements: default_max_elements(),
417        }
418    }
419}
420
421/// DiskANN-specific vector configuration.
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct VectorDiskAnnConfig {
424    /// Maximum number of points the index can hold.
425    #[serde(default = "default_diskann_max_points")]
426    pub max_points: usize,
427
428    /// Vector dimensionality.
429    #[serde(default = "default_diskann_dimensions")]
430    pub dimensions: usize,
431
432    /// Number of neighbors per node in the DiskANN graph.
433    #[serde(default = "default_diskann_num_neighbors")]
434    pub num_neighbors: usize,
435
436    /// Size of the search candidate list.
437    #[serde(default = "default_diskann_search_list_size")]
438    pub search_list_size: usize,
439
440    /// Directory path for SSD-backed data files.
441    #[serde(default = "default_diskann_data_path")]
442    pub data_path: String,
443
444    /// Whether to use product quantization for compression.
445    #[serde(default = "default_diskann_use_pq")]
446    pub use_pq: bool,
447
448    /// Number of PQ sub-quantizer chunks.
449    #[serde(default = "default_diskann_pq_num_chunks")]
450    pub pq_num_chunks: usize,
451}
452
453fn default_diskann_max_points() -> usize {
454    10_000_000
455}
456fn default_diskann_dimensions() -> usize {
457    384
458}
459fn default_diskann_num_neighbors() -> usize {
460    64
461}
462fn default_diskann_search_list_size() -> usize {
463    100
464}
465fn default_diskann_data_path() -> String {
466    ".weftos/diskann".to_owned()
467}
468fn default_diskann_use_pq() -> bool {
469    true
470}
471fn default_diskann_pq_num_chunks() -> usize {
472    48
473}
474
475impl Default for VectorDiskAnnConfig {
476    fn default() -> Self {
477        Self {
478            max_points: default_diskann_max_points(),
479            dimensions: default_diskann_dimensions(),
480            num_neighbors: default_diskann_num_neighbors(),
481            search_list_size: default_diskann_search_list_size(),
482            data_path: default_diskann_data_path(),
483            use_pq: default_diskann_use_pq(),
484            pq_num_chunks: default_diskann_pq_num_chunks(),
485        }
486    }
487}
488
489/// Eviction policy for the hybrid backend's hot tier.
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
491#[serde(rename_all = "lowercase")]
492pub enum VectorEvictionPolicy {
493    /// Least Recently Used.
494    #[default]
495    Lru,
496}
497
498/// Hybrid backend-specific configuration.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct VectorHybridConfig {
501    /// Maximum number of vectors in the hot (HNSW) tier.
502    #[serde(default = "default_hybrid_hot_capacity")]
503    pub hot_capacity: usize,
504
505    /// Access count threshold before a cold vector is promoted to hot.
506    #[serde(default = "default_hybrid_promotion_threshold")]
507    pub promotion_threshold: u32,
508
509    /// Eviction policy when the hot tier is full.
510    #[serde(default)]
511    pub eviction_policy: VectorEvictionPolicy,
512}
513
514fn default_hybrid_hot_capacity() -> usize {
515    50_000
516}
517fn default_hybrid_promotion_threshold() -> u32 {
518    3
519}
520
521impl Default for VectorHybridConfig {
522    fn default() -> Self {
523        Self {
524            hot_capacity: default_hybrid_hot_capacity(),
525            promotion_threshold: default_hybrid_promotion_threshold(),
526            eviction_policy: VectorEvictionPolicy::default(),
527        }
528    }
529}
530
531/// Unified vector search backend configuration.
532///
533/// Controls which backend is used for the ECC cognitive substrate's
534/// vector search layer.
535///
536/// # Example TOML
537///
538/// ```toml
539/// [kernel.vector]
540/// backend = "hybrid"
541///
542/// [kernel.vector.hnsw]
543/// ef_construction = 200
544/// max_elements = 100000
545///
546/// [kernel.vector.diskann]
547/// max_points = 10000000
548/// data_path = ".weftos/diskann"
549///
550/// [kernel.vector.hybrid]
551/// hot_capacity = 50000
552/// promotion_threshold = 3
553/// ```
554#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct VectorConfig {
556    /// Which backend to use.
557    #[serde(default)]
558    pub backend: VectorBackendKind,
559
560    /// HNSW-specific settings.
561    #[serde(default, skip_serializing_if = "Option::is_none")]
562    pub hnsw: Option<VectorHnswConfig>,
563
564    /// DiskANN-specific settings.
565    #[serde(default, skip_serializing_if = "Option::is_none")]
566    pub diskann: Option<VectorDiskAnnConfig>,
567
568    /// Hybrid-specific settings.
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    pub hybrid: Option<VectorHybridConfig>,
571}
572
573impl Default for VectorConfig {
574    fn default() -> Self {
575        Self {
576            backend: VectorBackendKind::default(),
577            hnsw: None,
578            diskann: None,
579            hybrid: None,
580        }
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    #[test]
589    fn default_kernel_config() {
590        let cfg = KernelConfig::default();
591        assert!(cfg.enabled);
592        assert_eq!(cfg.max_processes, 64);
593        assert_eq!(cfg.health_check_interval_secs, 30);
594    }
595
596    #[test]
597    fn deserialize_empty() {
598        let cfg: KernelConfig = serde_json::from_str("{}").unwrap();
599        assert!(cfg.enabled);
600        assert_eq!(cfg.max_processes, 64);
601    }
602
603    #[test]
604    fn deserialize_camel_case() {
605        let json = r#"{"maxProcesses": 128, "healthCheckIntervalSecs": 15}"#;
606        let cfg: KernelConfig = serde_json::from_str(json).unwrap();
607        assert_eq!(cfg.max_processes, 128);
608        assert_eq!(cfg.health_check_interval_secs, 15);
609    }
610
611    #[test]
612    fn serde_roundtrip() {
613        let cfg = KernelConfig {
614            enabled: true,
615            max_processes: 256,
616            health_check_interval_secs: 10,
617            cluster: None,
618            chain: None,
619            resource_tree: None,
620            vector: None,
621            profiles: None,
622            pairing: None,
623        };
624        let json = serde_json::to_string(&cfg).unwrap();
625        let restored: KernelConfig = serde_json::from_str(&json).unwrap();
626        assert_eq!(restored.enabled, cfg.enabled);
627        assert_eq!(restored.max_processes, cfg.max_processes);
628    }
629
630    #[test]
631    fn profiles_config_defaults() {
632        let cfg = ProfilesConfig::default();
633        assert!(cfg.enabled);
634        assert_eq!(cfg.storage_path, ".weftos/profiles");
635        assert_eq!(cfg.default_profile, "default");
636    }
637
638    #[test]
639    fn profiles_config_deserialize() {
640        let json = r#"{"enabled": false, "storage_path": "/tmp/profiles", "default_profile": "admin"}"#;
641        let cfg: ProfilesConfig = serde_json::from_str(json).unwrap();
642        assert!(!cfg.enabled);
643        assert_eq!(cfg.storage_path, "/tmp/profiles");
644        assert_eq!(cfg.default_profile, "admin");
645    }
646
647    #[test]
648    fn pairing_config_defaults() {
649        let cfg = PairingConfig::default();
650        assert_eq!(cfg.persist_path, ".weftos/runtime/paired_hosts.json");
651        assert_eq!(cfg.default_window_secs, 30);
652    }
653
654    #[test]
655    fn pairing_config_deserialize() {
656        let json = r#"{"persist_path": "/opt/pairing.json", "default_window_secs": 60}"#;
657        let cfg: PairingConfig = serde_json::from_str(json).unwrap();
658        assert_eq!(cfg.persist_path, "/opt/pairing.json");
659        assert_eq!(cfg.default_window_secs, 60);
660    }
661
662    #[test]
663    fn kernel_config_with_profiles_and_pairing() {
664        let json = r#"{"profiles": {"enabled": true}, "pairing": {"default_window_secs": 45}}"#;
665        let cfg: KernelConfig = serde_json::from_str(json).unwrap();
666        assert!(cfg.profiles.is_some());
667        assert!(cfg.profiles.unwrap().enabled);
668        assert!(cfg.pairing.is_some());
669        assert_eq!(cfg.pairing.unwrap().default_window_secs, 45);
670    }
671
672    #[test]
673    fn vector_config_defaults() {
674        let cfg = VectorConfig::default();
675        assert_eq!(cfg.backend, VectorBackendKind::Hnsw);
676        assert!(cfg.hnsw.is_none());
677        assert!(cfg.diskann.is_none());
678        assert!(cfg.hybrid.is_none());
679    }
680
681    #[test]
682    fn vector_config_deserialize_hybrid() {
683        let json = r#"{"backend": "hybrid", "hybrid": {"hot_capacity": 1000, "promotion_threshold": 5}}"#;
684        let cfg: VectorConfig = serde_json::from_str(json).unwrap();
685        assert_eq!(cfg.backend, VectorBackendKind::Hybrid);
686        let h = cfg.hybrid.unwrap();
687        assert_eq!(h.hot_capacity, 1000);
688        assert_eq!(h.promotion_threshold, 5);
689    }
690
691    #[test]
692    fn vector_config_deserialize_diskann() {
693        let json = r#"{"backend": "diskann", "diskann": {"max_points": 5000000}}"#;
694        let cfg: VectorConfig = serde_json::from_str(json).unwrap();
695        assert_eq!(cfg.backend, VectorBackendKind::DiskAnn);
696        let d = cfg.diskann.unwrap();
697        assert_eq!(d.max_points, 5_000_000);
698    }
699
700    #[test]
701    fn kernel_config_with_vector() {
702        let json = r#"{"vector": {"backend": "hnsw"}}"#;
703        let cfg: KernelConfig = serde_json::from_str(json).unwrap();
704        assert!(cfg.vector.is_some());
705        assert_eq!(cfg.vector.unwrap().backend, VectorBackendKind::Hnsw);
706    }
707}