1#![allow(missing_docs)]
2
3use serde::{Deserialize, Serialize};
4use std::env;
5use std::path::PathBuf;
6
7use crate::error::{Result, ShellError};
8use crate::path_utils::DEFAULT_FORBIDDEN_COMMANDS;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum FileOperationType {
13 Create,
14 Write,
15 Read,
16 Delete,
17 Mkdir,
18 Move,
19 Copy,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FileOperation {
24 #[serde(rename = "type")]
25 pub op_type: FileOperationType,
26 pub target: String,
27 pub secondary_target: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CommandResult {
32 pub success: bool,
33 pub stdout: String,
34 pub stderr: String,
35 pub exit_code: Option<i32>,
36 pub error: Option<String>,
37 pub executed_in: String,
38}
39
40impl CommandResult {
41 pub fn error(message: &str, stderr: &str, executed_in: &str) -> Self {
42 Self {
43 success: false,
44 stdout: String::new(),
45 stderr: stderr.to_string(),
46 exit_code: Some(1),
47 error: Some(message.to_string()),
48 executed_in: executed_in.to_string(),
49 }
50 }
51
52 pub fn success(stdout: String, executed_in: &str) -> Self {
53 Self {
54 success: true,
55 stdout,
56 stderr: String::new(),
57 exit_code: Some(0),
58 error: None,
59 executed_in: executed_in.to_string(),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CommandHistoryEntry {
66 pub command: String,
67 pub stdout: String,
68 pub stderr: String,
69 pub exit_code: Option<i32>,
70 pub timestamp: f64,
71 pub working_directory: String,
72 pub file_operations: Option<Vec<FileOperation>>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ShellConfig {
77 pub enabled: bool,
78 pub allowed_directory: PathBuf,
79 pub timeout_ms: u64,
80 pub forbidden_commands: Vec<String>,
81}
82
83impl Default for ShellConfig {
84 fn default() -> Self {
85 Self {
86 enabled: false,
87 allowed_directory: env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
88 timeout_ms: 30000,
89 forbidden_commands: DEFAULT_FORBIDDEN_COMMANDS
90 .iter()
91 .map(|s| s.to_string())
92 .collect(),
93 }
94 }
95}
96
97impl ShellConfig {
98 pub fn builder() -> ShellConfigBuilder {
99 ShellConfigBuilder::default()
100 }
101
102 pub fn from_env() -> Result<Self> {
103 let enabled = true;
104
105 let allowed_directory = env::var("SHELL_ALLOWED_DIRECTORY")
106 .map(PathBuf::from)
107 .unwrap_or_else(|_| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
108
109 let timeout_ms = env::var("SHELL_TIMEOUT")
110 .ok()
111 .and_then(|v| v.parse().ok())
112 .unwrap_or(30000);
113
114 let custom_forbidden: Vec<String> = env::var("SHELL_FORBIDDEN_COMMANDS")
115 .map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
116 .unwrap_or_default();
117
118 let mut forbidden_commands: Vec<String> = DEFAULT_FORBIDDEN_COMMANDS
119 .iter()
120 .map(|s| s.to_string())
121 .collect();
122 forbidden_commands.extend(custom_forbidden);
123 forbidden_commands.sort();
124 forbidden_commands.dedup();
125
126 Ok(Self {
127 enabled,
128 allowed_directory,
129 timeout_ms,
130 forbidden_commands,
131 })
132 }
133}
134
135#[derive(Debug, Default)]
136pub struct ShellConfigBuilder {
137 enabled: Option<bool>,
138 allowed_directory: Option<PathBuf>,
139 timeout_ms: Option<u64>,
140 forbidden_commands: Option<Vec<String>>,
141}
142
143impl ShellConfigBuilder {
144 pub fn enabled(mut self, enabled: bool) -> Self {
145 self.enabled = Some(enabled);
146 self
147 }
148
149 pub fn allowed_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
150 self.allowed_directory = Some(path.into());
151 self
152 }
153
154 pub fn timeout_ms(mut self, timeout: u64) -> Self {
155 self.timeout_ms = Some(timeout);
156 self
157 }
158
159 pub fn forbidden_commands(mut self, commands: Vec<String>) -> Self {
160 self.forbidden_commands = Some(commands);
161 self
162 }
163
164 pub fn add_forbidden_command(mut self, command: String) -> Self {
165 let mut commands = self.forbidden_commands.unwrap_or_default();
166 commands.push(command);
167 self.forbidden_commands = Some(commands);
168 self
169 }
170
171 pub fn build(self) -> Result<ShellConfig> {
172 let allowed_directory = self
173 .allowed_directory
174 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
175
176 if !allowed_directory.exists() {
177 return Err(ShellError::Config(format!(
178 "Allowed directory does not exist: {}",
179 allowed_directory.display()
180 )));
181 }
182
183 if !allowed_directory.is_dir() {
184 return Err(ShellError::Config(format!(
185 "Allowed path is not a directory: {}",
186 allowed_directory.display()
187 )));
188 }
189
190 let mut forbidden_commands: Vec<String> = DEFAULT_FORBIDDEN_COMMANDS
191 .iter()
192 .map(|s| s.to_string())
193 .collect();
194
195 if let Some(custom) = self.forbidden_commands {
196 forbidden_commands.extend(custom);
197 }
198
199 forbidden_commands.sort();
200 forbidden_commands.dedup();
201
202 Ok(ShellConfig {
203 enabled: self.enabled.unwrap_or(false),
204 allowed_directory,
205 timeout_ms: self.timeout_ms.unwrap_or(30000),
206 forbidden_commands,
207 })
208 }
209}