Skip to main content

lean_ctx/core/config/
risk.rs

1//! Risk classification for `config set` governance (#852).
2//!
3//! Most config keys are routine (cache sizes, density, theme) and are written
4//! without friction. A small set, however, changes lean-ctx's *security posture*
5//! (containment, secret redaction) or *network routing / data egress* (upstream
6//! redirects). Flipping one of those silently can weaken the user's machine or
7//! leak credentials to a provider, so `config set` shows a before→after review
8//! and requires confirmation (or `--yes`) before applying — mirroring the
9//! existing `yolo` / `secure` confirmation pattern.
10//!
11//! This is a deterministic, local-only lookup: no telemetry, no heuristics.
12
13/// A consequential config key and the one-line note explaining what changing it
14/// does. Returned by [`classify`]; `None` ⇒ a routine key, written directly.
15pub struct ConfigRisk {
16    /// Human-readable consequence of changing this key, shown in the review.
17    pub note: &'static str,
18}
19
20/// Classifies a fully-qualified config key (dot-path, e.g. `secret_detection.enabled`).
21///
22/// Returns a [`ConfigRisk`] for keys whose change is security- or
23/// egress-relevant; `None` for everything else.
24#[must_use]
25pub fn classify(key: &str) -> Option<ConfigRisk> {
26    let note = match key {
27        "path_jail" => {
28            "Path jail confines agent file access to the project root. Disabling it lets tools read and write any path on this machine."
29        }
30        "shell_security" => {
31            "Shell gating blocks dangerous commands via an allowlist. Lowering it (warn/off) lets the agent run any command."
32        }
33        "sandbox_level" => {
34            "Sandbox level governs how strictly tool execution is contained. Lowering it reduces isolation."
35        }
36        "secret_detection.enabled" => {
37            "Secret detection masks API keys and .env values before they reach the LLM. Disabling it can leak credentials to the provider."
38        }
39        "secret_detection.redact" => {
40            "Secret redaction masks detected secrets. Disabling it sends them verbatim to the provider."
41        }
42        "boundary_policy" => {
43            "Boundary policy controls what context is allowed to leave this machine. Relaxing it widens data egress."
44        }
45        "proxy_require_token" => {
46            "Proxy token policy controls whether provider API keys can authenticate local proxy requests without the lean-ctx Bearer token."
47        }
48        "proxy.anthropic_upstream"
49        | "proxy.openai_upstream"
50        | "proxy.chatgpt_upstream"
51        | "proxy.gemini_upstream" => {
52            "Redirects provider traffic to a custom upstream — every request and API key for this provider will flow through it."
53        }
54        _ => return None,
55    };
56    Some(ConfigRisk { note })
57}
58
59/// True if changing `key` is consequential enough to require a review.
60#[must_use]
61pub fn is_consequential(key: &str) -> bool {
62    classify(key).is_some()
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn security_keys_are_consequential() {
71        for key in [
72            "path_jail",
73            "shell_security",
74            "sandbox_level",
75            "secret_detection.enabled",
76            "secret_detection.redact",
77            "boundary_policy",
78            "proxy_require_token",
79            "proxy.openai_upstream",
80            "proxy.chatgpt_upstream",
81            "proxy.anthropic_upstream",
82            "proxy.gemini_upstream",
83        ] {
84            assert!(is_consequential(key), "{key} should be consequential");
85            assert!(!classify(key).unwrap().note.is_empty());
86        }
87    }
88
89    #[test]
90    fn routine_keys_are_not_consequential() {
91        for key in [
92            "theme",
93            "max_ram_percent",
94            "compression_level",
95            "proxy.port",
96            "proxy.effort",
97            "bm25_max_cache_mb",
98        ] {
99            assert!(!is_consequential(key), "{key} should be routine");
100        }
101    }
102}