Skip to main content

elizaos_plugin_shell/
types.rs

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}