lean_ctx/cli/
allow_cmd.rs1use crate::core::config;
10use crate::core::shell_allowlist;
11
12pub fn cmd_allow(args: &[String]) {
13 match args.first().map(std::string::String::as_str) {
14 None | Some("--help" | "-h") => print_usage(),
15 Some("--list" | "list" | "ls") => print_effective(),
16 Some("--remove" | "-r" | "remove" | "rm") => remove(&args[1..]),
17 _ => add(args),
18 }
19}
20
21fn add(cmds: &[String]) {
23 let requested: Vec<String> = cmds
24 .iter()
25 .map(|c| c.trim().to_string())
26 .filter(|c| !c.is_empty())
27 .collect();
28
29 if requested.is_empty() {
30 print_usage();
31 return;
32 }
33
34 let mut extra = current_extra_from_global();
35 let mut added = Vec::new();
36 for cmd in requested {
37 if extra.iter().any(|e| e == &cmd) {
38 println!(" already allowed: {cmd}");
39 } else {
40 extra.push(cmd.clone());
41 added.push(cmd);
42 }
43 }
44
45 if added.is_empty() {
46 println!("\nNothing to add — all commands were already in the allowlist.");
47 print_effective();
48 return;
49 }
50
51 if let Err(e) = write_extra(&extra) {
52 eprintln!("Error: {e}");
53 std::process::exit(1);
54 }
55
56 println!("Allowed (additive): {}", added.join(", "));
57 println!("These are merged on top of the defaults — nothing else was removed.");
58 println!("Takes effect immediately; no MCP/daemon restart needed.");
59 print_effective();
60}
61
62fn remove(cmds: &[String]) {
64 let to_remove: Vec<String> = cmds
65 .iter()
66 .map(|c| c.trim().to_string())
67 .filter(|c| !c.is_empty())
68 .collect();
69
70 if to_remove.is_empty() {
71 eprintln!("Usage: lean-ctx allow --remove <cmd> [<cmd>...]");
72 std::process::exit(1);
73 }
74
75 let before = current_extra_from_global();
76 let after: Vec<String> = before
77 .iter()
78 .filter(|e| !to_remove.iter().any(|r| r == *e))
79 .cloned()
80 .collect();
81
82 let removed: Vec<&String> = before.iter().filter(|e| !after.contains(e)).collect();
83 if removed.is_empty() {
84 println!("None of those were in shell_allowlist_extra (nothing changed).");
85 println!("Note: built-in defaults can't be removed here — set `shell_allowlist` explicitly to override the whole list.");
86 return;
87 }
88
89 if let Err(e) = write_extra(&after) {
90 eprintln!("Error: {e}");
91 std::process::exit(1);
92 }
93
94 let names: Vec<&str> = removed.iter().map(|s| s.as_str()).collect();
95 println!("Removed from extra allowlist: {}", names.join(", "));
96 print_effective();
97}
98
99fn print_effective() {
103 let effective = shell_allowlist::effective_allowlist_pub();
104 let parse_err = config::last_config_parse_error();
105 let path = config::Config::path().map_or_else(
106 || "~/.lean-ctx/config.toml".to_string(),
107 |p| p.display().to_string(),
108 );
109
110 println!("\nShell allowlist (enforced by the MCP tools):");
111 println!(" Config: {path}");
112
113 if let Some(err) = parse_err {
114 println!(" \x1b[31m⚠ config.toml FAILED to parse — running on DEFAULTS.\x1b[0m");
115 println!(" {err}");
116 println!(" Fix the TOML above, then re-run `lean-ctx allow --list`.");
117 }
118
119 if effective.is_empty() {
120 println!(" Mode: disabled (every command is allowed)");
121 return;
122 }
123
124 println!(
125 " Mode: restricted — {} command(s) permitted",
126 effective.len()
127 );
128
129 let extra = current_extra_from_global();
130 if extra.is_empty() {
131 println!(" Extra (additive, via `lean-ctx allow`): none");
132 } else {
133 println!(
134 " Extra (additive, via `lean-ctx allow`): {}",
135 extra.join(", ")
136 );
137 }
138}
139
140fn current_extra_from_global() -> Vec<String> {
143 let Some(path) = config::Config::path() else {
144 return Vec::new();
145 };
146 let Ok(raw) = std::fs::read_to_string(&path) else {
147 return Vec::new();
148 };
149 let Ok(table) = raw.parse::<toml::Table>() else {
150 return Vec::new();
151 };
152 table
153 .get("shell_allowlist_extra")
154 .and_then(toml::Value::as_array)
155 .map(|arr| {
156 arr.iter()
157 .filter_map(|v| v.as_str().map(str::to_string))
158 .collect()
159 })
160 .unwrap_or_default()
161}
162
163fn write_extra(extra: &[String]) -> Result<(), String> {
165 config::setter::set_by_key("shell_allowlist_extra", &extra.join(",")).map(|_| ())
166}
167
168fn print_usage() {
169 println!(
170 "Usage: lean-ctx allow <cmd> [<cmd>...] Add command(s) to the shell allowlist (additive)\n\
171 \x20 lean-ctx allow --list Show the effective allowlist + config path\n\
172 \x20 lean-ctx allow --remove <cmd> Remove command(s) you previously added\n\
173 \n\
174 Why this exists: editing `shell_allowlist` replaces the whole built-in list.\n\
175 `lean-ctx allow` appends to `shell_allowlist_extra`, keeping git/cargo/npm/… intact.\n\
176 Example: lean-ctx allow acli"
177 );
178 print_effective();
179}