Skip to main content

kanade_shared/
config.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4use serde::Deserialize;
5
6// ─── Agent config ────────────────────────────────────────────────────
7
8#[derive(Deserialize, Debug, Clone)]
9pub struct AgentConfig {
10    pub agent: AgentSection,
11    pub log: LogSection,
12    /// DEPRECATED in Sprint 6: inventory cadence / jitter / enabled
13    /// are now sourced from the layered `agent_config` KV bucket.
14    /// Still parsed (back-compat for existing agent.toml files);
15    /// the value is logged-and-ignored at startup. Field removal
16    /// is scheduled for v0.4.0 (one minor cycle after this
17    /// deprecation lands in v0.3.0).
18    #[serde(default)]
19    pub inventory: InventorySection,
20}
21
22#[derive(Deserialize, Debug, Clone)]
23pub struct AgentSection {
24    pub id: String,
25    pub nats_url: String,
26    /// DEPRECATED in Sprint 5: group membership is now server-managed
27    /// via the `agent_groups` KV bucket. Use
28    /// `kanade agent groups set <pc_id> <group> [<group> ...]` to
29    /// declare membership. Still parsed for back-compat; the value
30    /// is logged-and-ignored at startup. Field removal is scheduled
31    /// for v0.4.0.
32    #[serde(default)]
33    pub groups: Vec<String>,
34}
35
36#[derive(Deserialize, Debug, Clone)]
37pub struct LogSection {
38    pub path: String,
39    pub level: String,
40    /// Number of rotated daily files (incl. today's) to retain.
41    /// Defaults to 14 — covers two weeks of incidents without
42    /// blowing up disk. Set to 0 to disable on-disk logging
43    /// (stdout only).
44    #[serde(default = "default_keep_days")]
45    pub keep_days: usize,
46}
47
48fn default_keep_days() -> usize {
49    14
50}
51
52#[derive(Deserialize, Debug, Clone)]
53pub struct InventorySection {
54    #[serde(default = "default_hw_interval")]
55    pub hw_interval: String,
56    #[serde(default = "default_jitter")]
57    pub jitter: String,
58    #[serde(default = "default_enabled")]
59    pub enabled: bool,
60}
61
62impl Default for InventorySection {
63    fn default() -> Self {
64        Self {
65            hw_interval: default_hw_interval(),
66            jitter: default_jitter(),
67            enabled: default_enabled(),
68        }
69    }
70}
71
72fn default_hw_interval() -> String {
73    "24h".into()
74}
75fn default_jitter() -> String {
76    "10m".into()
77}
78fn default_enabled() -> bool {
79    true
80}
81
82// ─── Backend config ──────────────────────────────────────────────────
83
84#[derive(Deserialize, Debug, Clone)]
85pub struct BackendConfig {
86    pub server: ServerSection,
87    pub nats: NatsSection,
88    pub db: DbSection,
89    pub log: LogSection,
90}
91
92#[derive(Deserialize, Debug, Clone)]
93pub struct ServerSection {
94    pub bind: String,
95}
96
97#[derive(Deserialize, Debug, Clone)]
98pub struct NatsSection {
99    pub url: String,
100}
101
102#[derive(Deserialize, Debug, Clone)]
103pub struct DbSection {
104    pub sqlite_path: String,
105}
106
107// ─── Loader ──────────────────────────────────────────────────────────
108
109fn load_typed<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
110    let mut engine = teravars::Engine::new();
111    let ctx = teravars::system_context();
112    let paths: Vec<PathBuf> = vec![path.to_path_buf()];
113    let merged = teravars::load_merged(&paths, &mut engine, &ctx)
114        .with_context(|| format!("teravars load_merged: {path:?}"))?;
115    let cfg: T = toml::Value::Table(merged.config)
116        .try_into()
117        .with_context(|| format!("decode config from {path:?}"))?;
118    Ok(cfg)
119}
120
121pub fn load_agent_config(path: &Path) -> Result<AgentConfig> {
122    load_typed(path)
123}
124
125pub fn load_backend_config(path: &Path) -> Result<BackendConfig> {
126    load_typed(path)
127}