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
155impl Default for KernelConfig {
156    fn default() -> Self {
157        Self {
158            enabled: true,
159            max_processes: default_max_processes(),
160            health_check_interval_secs: default_health_check_interval_secs(),
161            cluster: None,
162            chain: None,
163            resource_tree: None,
164            vector: None,
165        }
166    }
167}
168
169/// Local chain configuration.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ChainConfig {
172    /// Whether the local chain is enabled.
173    #[serde(default = "default_true")]
174    pub enabled: bool,
175
176    /// Maximum events before auto-checkpoint.
177    #[serde(default = "default_checkpoint_interval", alias = "checkpointInterval")]
178    pub checkpoint_interval: u64,
179
180    /// Chain ID (0 = local node chain).
181    #[serde(default)]
182    pub chain_id: u32,
183
184    /// Path to the chain checkpoint file for persistence across restarts.
185    /// If `None`, defaults to `~/.clawft/chain/local.json`.
186    #[serde(
187        default,
188        skip_serializing_if = "Option::is_none",
189        alias = "checkpointPath"
190    )]
191    pub checkpoint_path: Option<String>,
192}
193
194fn default_true() -> bool {
195    true
196}
197fn default_checkpoint_interval() -> u64 {
198    1000
199}
200
201impl Default for ChainConfig {
202    fn default() -> Self {
203        Self {
204            enabled: true,
205            checkpoint_interval: default_checkpoint_interval(),
206            chain_id: 0,
207            checkpoint_path: None,
208        }
209    }
210}
211
212impl ChainConfig {
213    /// Returns the effective checkpoint path.
214    ///
215    /// If `checkpoint_path` is set, returns it. Otherwise falls back to
216    /// `~/.clawft/chain.json` (requires the `native` feature for `dirs`).
217    pub fn effective_checkpoint_path(&self) -> Option<String> {
218        if self.checkpoint_path.is_some() {
219            return self.checkpoint_path.clone();
220        }
221        #[cfg(feature = "native")]
222        {
223            dirs::home_dir().map(|h| {
224                h.join(".clawft")
225                    .join("chain.json")
226                    .to_string_lossy()
227                    .into_owned()
228            })
229        }
230        #[cfg(not(feature = "native"))]
231        {
232            None
233        }
234    }
235}
236
237/// Resource tree configuration.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct ResourceTreeConfig {
240    /// Whether the resource tree is enabled.
241    #[serde(default = "default_true_rt")]
242    pub enabled: bool,
243
244    /// Path to checkpoint file (None = in-memory only).
245    #[serde(
246        default,
247        skip_serializing_if = "Option::is_none",
248        alias = "checkpointPath"
249    )]
250    pub checkpoint_path: Option<String>,
251}
252
253fn default_true_rt() -> bool {
254    true
255}
256
257impl Default for ResourceTreeConfig {
258    fn default() -> Self {
259        Self {
260            enabled: true,
261            checkpoint_path: None,
262        }
263    }
264}
265
266// ── Vector search backend configuration ──────────────────────────────────
267
268/// Which vector search backend to use.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
270#[serde(rename_all = "lowercase")]
271pub enum VectorBackendKind {
272    /// In-memory HNSW (default, fast, suitable for <1M vectors).
273    #[default]
274    Hnsw,
275    /// SSD-backed DiskANN (large scale, 1M+ vectors).
276    DiskAnn,
277    /// Hot HNSW cache + cold DiskANN store.
278    Hybrid,
279}
280
281/// HNSW-specific vector configuration.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct VectorHnswConfig {
284    /// ef_construction parameter for index building.
285    #[serde(default = "default_ef_construction")]
286    pub ef_construction: usize,
287
288    /// Number of bi-directional links per node (M parameter).
289    #[serde(default = "default_m")]
290    pub m: usize,
291
292    /// Maximum number of elements the index can hold.
293    #[serde(default = "default_max_elements")]
294    pub max_elements: usize,
295}
296
297fn default_ef_construction() -> usize {
298    200
299}
300fn default_m() -> usize {
301    16
302}
303fn default_max_elements() -> usize {
304    100_000
305}
306
307impl Default for VectorHnswConfig {
308    fn default() -> Self {
309        Self {
310            ef_construction: default_ef_construction(),
311            m: default_m(),
312            max_elements: default_max_elements(),
313        }
314    }
315}
316
317/// DiskANN-specific vector configuration.
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct VectorDiskAnnConfig {
320    /// Maximum number of points the index can hold.
321    #[serde(default = "default_diskann_max_points")]
322    pub max_points: usize,
323
324    /// Vector dimensionality.
325    #[serde(default = "default_diskann_dimensions")]
326    pub dimensions: usize,
327
328    /// Number of neighbors per node in the DiskANN graph.
329    #[serde(default = "default_diskann_num_neighbors")]
330    pub num_neighbors: usize,
331
332    /// Size of the search candidate list.
333    #[serde(default = "default_diskann_search_list_size")]
334    pub search_list_size: usize,
335
336    /// Directory path for SSD-backed data files.
337    #[serde(default = "default_diskann_data_path")]
338    pub data_path: String,
339
340    /// Whether to use product quantization for compression.
341    #[serde(default = "default_diskann_use_pq")]
342    pub use_pq: bool,
343
344    /// Number of PQ sub-quantizer chunks.
345    #[serde(default = "default_diskann_pq_num_chunks")]
346    pub pq_num_chunks: usize,
347}
348
349fn default_diskann_max_points() -> usize {
350    10_000_000
351}
352fn default_diskann_dimensions() -> usize {
353    384
354}
355fn default_diskann_num_neighbors() -> usize {
356    64
357}
358fn default_diskann_search_list_size() -> usize {
359    100
360}
361fn default_diskann_data_path() -> String {
362    ".weftos/diskann".to_owned()
363}
364fn default_diskann_use_pq() -> bool {
365    true
366}
367fn default_diskann_pq_num_chunks() -> usize {
368    48
369}
370
371impl Default for VectorDiskAnnConfig {
372    fn default() -> Self {
373        Self {
374            max_points: default_diskann_max_points(),
375            dimensions: default_diskann_dimensions(),
376            num_neighbors: default_diskann_num_neighbors(),
377            search_list_size: default_diskann_search_list_size(),
378            data_path: default_diskann_data_path(),
379            use_pq: default_diskann_use_pq(),
380            pq_num_chunks: default_diskann_pq_num_chunks(),
381        }
382    }
383}
384
385/// Eviction policy for the hybrid backend's hot tier.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
387#[serde(rename_all = "lowercase")]
388pub enum VectorEvictionPolicy {
389    /// Least Recently Used.
390    #[default]
391    Lru,
392}
393
394/// Hybrid backend-specific configuration.
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct VectorHybridConfig {
397    /// Maximum number of vectors in the hot (HNSW) tier.
398    #[serde(default = "default_hybrid_hot_capacity")]
399    pub hot_capacity: usize,
400
401    /// Access count threshold before a cold vector is promoted to hot.
402    #[serde(default = "default_hybrid_promotion_threshold")]
403    pub promotion_threshold: u32,
404
405    /// Eviction policy when the hot tier is full.
406    #[serde(default)]
407    pub eviction_policy: VectorEvictionPolicy,
408}
409
410fn default_hybrid_hot_capacity() -> usize {
411    50_000
412}
413fn default_hybrid_promotion_threshold() -> u32 {
414    3
415}
416
417impl Default for VectorHybridConfig {
418    fn default() -> Self {
419        Self {
420            hot_capacity: default_hybrid_hot_capacity(),
421            promotion_threshold: default_hybrid_promotion_threshold(),
422            eviction_policy: VectorEvictionPolicy::default(),
423        }
424    }
425}
426
427/// Unified vector search backend configuration.
428///
429/// Controls which backend is used for the ECC cognitive substrate's
430/// vector search layer.
431///
432/// # Example TOML
433///
434/// ```toml
435/// [kernel.vector]
436/// backend = "hybrid"
437///
438/// [kernel.vector.hnsw]
439/// ef_construction = 200
440/// max_elements = 100000
441///
442/// [kernel.vector.diskann]
443/// max_points = 10000000
444/// data_path = ".weftos/diskann"
445///
446/// [kernel.vector.hybrid]
447/// hot_capacity = 50000
448/// promotion_threshold = 3
449/// ```
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct VectorConfig {
452    /// Which backend to use.
453    #[serde(default)]
454    pub backend: VectorBackendKind,
455
456    /// HNSW-specific settings.
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub hnsw: Option<VectorHnswConfig>,
459
460    /// DiskANN-specific settings.
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub diskann: Option<VectorDiskAnnConfig>,
463
464    /// Hybrid-specific settings.
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub hybrid: Option<VectorHybridConfig>,
467}
468
469impl Default for VectorConfig {
470    fn default() -> Self {
471        Self {
472            backend: VectorBackendKind::default(),
473            hnsw: None,
474            diskann: None,
475            hybrid: None,
476        }
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn default_kernel_config() {
486        let cfg = KernelConfig::default();
487        assert!(cfg.enabled);
488        assert_eq!(cfg.max_processes, 64);
489        assert_eq!(cfg.health_check_interval_secs, 30);
490    }
491
492    #[test]
493    fn deserialize_empty() {
494        let cfg: KernelConfig = serde_json::from_str("{}").unwrap();
495        assert!(cfg.enabled);
496        assert_eq!(cfg.max_processes, 64);
497    }
498
499    #[test]
500    fn deserialize_camel_case() {
501        let json = r#"{"maxProcesses": 128, "healthCheckIntervalSecs": 15}"#;
502        let cfg: KernelConfig = serde_json::from_str(json).unwrap();
503        assert_eq!(cfg.max_processes, 128);
504        assert_eq!(cfg.health_check_interval_secs, 15);
505    }
506
507    #[test]
508    fn serde_roundtrip() {
509        let cfg = KernelConfig {
510            enabled: true,
511            max_processes: 256,
512            health_check_interval_secs: 10,
513            cluster: None,
514            chain: None,
515            resource_tree: None,
516            vector: None,
517        };
518        let json = serde_json::to_string(&cfg).unwrap();
519        let restored: KernelConfig = serde_json::from_str(&json).unwrap();
520        assert_eq!(restored.enabled, cfg.enabled);
521        assert_eq!(restored.max_processes, cfg.max_processes);
522    }
523
524    #[test]
525    fn vector_config_defaults() {
526        let cfg = VectorConfig::default();
527        assert_eq!(cfg.backend, VectorBackendKind::Hnsw);
528        assert!(cfg.hnsw.is_none());
529        assert!(cfg.diskann.is_none());
530        assert!(cfg.hybrid.is_none());
531    }
532
533    #[test]
534    fn vector_config_deserialize_hybrid() {
535        let json = r#"{"backend": "hybrid", "hybrid": {"hot_capacity": 1000, "promotion_threshold": 5}}"#;
536        let cfg: VectorConfig = serde_json::from_str(json).unwrap();
537        assert_eq!(cfg.backend, VectorBackendKind::Hybrid);
538        let h = cfg.hybrid.unwrap();
539        assert_eq!(h.hot_capacity, 1000);
540        assert_eq!(h.promotion_threshold, 5);
541    }
542
543    #[test]
544    fn vector_config_deserialize_diskann() {
545        let json = r#"{"backend": "diskann", "diskann": {"max_points": 5000000}}"#;
546        let cfg: VectorConfig = serde_json::from_str(json).unwrap();
547        assert_eq!(cfg.backend, VectorBackendKind::DiskAnn);
548        let d = cfg.diskann.unwrap();
549        assert_eq!(d.max_points, 5_000_000);
550    }
551
552    #[test]
553    fn kernel_config_with_vector() {
554        let json = r#"{"vector": {"backend": "hnsw"}}"#;
555        let cfg: KernelConfig = serde_json::from_str(json).unwrap();
556        assert!(cfg.vector.is_some());
557        assert_eq!(cfg.vector.unwrap().backend, VectorBackendKind::Hnsw);
558    }
559}