Skip to main content

deck_core/
config.rs

1//! User-facing configuration. Resolved from (precedence high → low):
2//!   1. CLI flags
3//!   2. environment variables prefixed with `ONOSENDAI_`
4//!   3. TOML config file (`$XDG_CONFIG_HOME/ono-sendai/config.toml`)
5//!   4. compiled-in defaults
6//!
7//! The resolver lives in the binary crate; this module only declares the
8//! shape so that every other crate can reason about it.
9
10use std::path::PathBuf;
11
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize, Default)]
15pub struct Config {
16    pub llm: LlmConfig,
17    pub mcp: McpConfig,
18    pub store: StoreConfig,
19    pub sandbox: SandboxConfig,
20    pub tui: TuiConfig,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct LlmConfig {
25    /// Backend selector: "ollama" or "llama-cpp".
26    pub backend: String,
27    /// Endpoint URL (HTTP). Ignored for `llama-cpp` (which is in-process).
28    pub endpoint: String,
29    /// Default model name.
30    pub model: String,
31    /// Optional per-request timeout in seconds.
32    pub timeout_secs: u64,
33}
34
35impl Default for LlmConfig {
36    fn default() -> Self {
37        Self {
38            backend: "ollama".into(),
39            endpoint: "http://127.0.0.1:11434".into(),
40            model: "llama3.1".into(),
41            timeout_secs: 120,
42        }
43    }
44}
45
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct McpConfig {
48    /// Declared MCP servers. Each launches as a child process (stdio transport).
49    #[serde(default)]
50    pub servers: Vec<McpServerSpec>,
51    /// Approval popup timeout. 0 = require explicit approval, no auto-deny.
52    #[serde(default = "default_approval_timeout")]
53    pub approval_timeout_secs: u64,
54}
55
56const fn default_approval_timeout() -> u64 {
57    30
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct McpServerSpec {
62    pub name: String,
63    pub command: String,
64    #[serde(default)]
65    pub args: Vec<String>,
66    /// If `true`, launch this server through `deck-sandbox`.
67    #[serde(default = "default_true")]
68    pub sandbox: bool,
69    /// Optional path to a seccomp/landlock profile.
70    pub profile: Option<PathBuf>,
71}
72
73const fn default_true() -> bool {
74    true
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, Default)]
78pub struct StoreConfig {
79    /// Root directory for encrypted decks (default: `$XDG_DATA_HOME/ono-sendai/decks`).
80    pub root: Option<PathBuf>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SandboxConfig {
85    /// If `true`, refuse to run an MCP server whose `sandbox` flag is false
86    /// on platforms that support sandboxing. Defaults to true.
87    pub strict: bool,
88}
89
90impl Default for SandboxConfig {
91    fn default() -> Self {
92        Self { strict: true }
93    }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct TuiConfig {
98    /// Frame tick in milliseconds. 16ms ≈ 60fps.
99    pub tick_ms: u64,
100    /// Mouse capture toggle.
101    pub mouse: bool,
102}
103
104impl Default for TuiConfig {
105    fn default() -> Self {
106        Self {
107            tick_ms: 16,
108            mouse: true,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn defaults_roundtrip_through_toml() {
119        let cfg = Config::default();
120        let s = toml::to_string(&cfg).expect("serialize default config to toml");
121        let back: Config = toml::from_str(&s).expect("parse back default config");
122        assert_eq!(back.llm.backend, cfg.llm.backend);
123        assert_eq!(back.tui.tick_ms, cfg.tui.tick_ms);
124    }
125}