Skip to main content

lean_ctx/core/config/
shell_approval.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Whether `ctx_shell` may ask the user to approve a command the shell
3//! allowlist does not name (see `core::shell_allowlist::approval`).
4//!
5//! Global only: a cloned repository's `.lean-ctx.toml` never changes how the
6//! user is asked, so the key is not in the project merge.
7
8use serde::{Deserialize, Serialize};
9
10use super::Config;
11
12/// Approval prompts for commands outside the shell allowlist.
13///
14/// - `Ask` (default): when the MCP client supports form elicitation, a command
15///   refused only for unlisted programs is put to the user (once, this
16///   session, always, or deny). Hard blocks are never asked about.
17/// - `Off`: refuse as before, without asking.
18#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "kebab-case")]
20pub enum ShellApproval {
21    #[default]
22    Ask,
23    Off,
24}
25
26impl ShellApproval {
27    /// Parse `LEAN_CTX_SHELL_APPROVAL` (`ask|off` plus the usual booleans).
28    pub fn from_env() -> Option<Self> {
29        std::env::var("LEAN_CTX_SHELL_APPROVAL").ok().and_then(|v| {
30            match v.trim().to_lowercase().as_str() {
31                "ask" | "on" | "1" | "true" | "yes" => Some(Self::Ask),
32                "off" | "0" | "false" | "no" => Some(Self::Off),
33                _ => None,
34            }
35        })
36    }
37
38    /// Env override (`LEAN_CTX_SHELL_APPROVAL`) wins over the config value.
39    pub fn effective(config: &Config) -> Self {
40        Self::from_env().unwrap_or(config.shell_approval)
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn asks_by_default_and_parses_the_env_spellings() {
50        assert_eq!(ShellApproval::default(), ShellApproval::Ask);
51        let _lock = crate::core::data_dir::test_env_lock();
52        for (raw, want) in [
53            ("ask", Some(ShellApproval::Ask)),
54            ("OFF", Some(ShellApproval::Off)),
55            ("0", Some(ShellApproval::Off)),
56            ("maybe", None),
57        ] {
58            crate::test_env::set_var("LEAN_CTX_SHELL_APPROVAL", raw);
59            assert_eq!(ShellApproval::from_env(), want, "{raw}");
60        }
61        crate::test_env::remove_var("LEAN_CTX_SHELL_APPROVAL");
62    }
63
64    #[test]
65    fn the_config_key_reads_kebab_case() {
66        let cfg: Config = toml::from_str("shell_approval = \"off\"").unwrap();
67        assert_eq!(cfg.shell_approval, ShellApproval::Off);
68    }
69}