1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use bytes::Bytes;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub enum Message {
12 Request(Request),
14 Response(Response),
16}
17
18impl Message {
19 pub fn request(req: Request) -> Self {
21 Self::Request(req)
22 }
23
24 pub fn response(resp: Response) -> Self {
26 Self::Response(resp)
27 }
28
29 pub fn request_id(&self) -> Option<Uuid> {
31 match self {
32 Self::Request(req) => Some(req.id()),
33 Self::Response(resp) => Some(resp.request_id()),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum Request {
41 ProcessExec {
43 id: Uuid,
45 command: Vec<String>,
47 env: HashMap<String, String>,
49 cwd: Option<PathBuf>,
51 stdin: Option<Bytes>,
53 timeout: Option<u64>,
55 },
56
57 FileGet {
59 id: Uuid,
61 path: PathBuf,
63 range: Option<(u64, u64)>,
65 },
66
67 FilePut {
69 id: Uuid,
71 path: PathBuf,
73 content: Bytes,
75 mode: Option<u32>,
77 create_dirs: bool,
79 },
80
81 DirList {
83 id: Uuid,
85 path: PathBuf,
87 include_hidden: bool,
89 recursive: bool,
91 },
92
93 WasmExec {
95 id: Uuid,
97 module: Bytes,
99 input: Bytes,
101 timeout: Option<u64>,
103 },
104
105 JsonCall {
107 id: Uuid,
109 method: String,
111 params: Bytes,
113 },
114
115 Ping {
117 id: Uuid,
119 timestamp: u64,
121 },
122
123 PtyExec {
125 id: Uuid,
127 command: Vec<String>,
129 env: HashMap<String, String>,
131 cwd: Option<PathBuf>,
133 privilege: Option<PrivilegeEscalation>,
135 timeout: Option<u64>,
137 },
138}
139
140impl Request {
141 pub fn id(&self) -> Uuid {
143 match self {
144 Self::ProcessExec { id, .. } => *id,
145 Self::FileGet { id, .. } => *id,
146 Self::FilePut { id, .. } => *id,
147 Self::DirList { id, .. } => *id,
148 Self::WasmExec { id, .. } => *id,
149 Self::JsonCall { id, .. } => *id,
150 Self::Ping { id, .. } => *id,
151 Self::PtyExec { id, .. } => *id,
152 }
153 }
154
155 pub fn process_exec(
157 command: Vec<String>,
158 env: HashMap<String, String>,
159 cwd: Option<PathBuf>,
160 stdin: Option<Bytes>,
161 timeout: Option<u64>,
162 ) -> Self {
163 Self::ProcessExec {
164 id: Uuid::new_v4(),
165 command,
166 env,
167 cwd,
168 stdin,
169 timeout,
170 }
171 }
172
173 pub fn file_get(path: PathBuf, range: Option<(u64, u64)>) -> Self {
175 Self::FileGet {
176 id: Uuid::new_v4(),
177 path,
178 range,
179 }
180 }
181
182 pub fn file_put(path: PathBuf, content: Bytes, mode: Option<u32>, create_dirs: bool) -> Self {
184 Self::FilePut {
185 id: Uuid::new_v4(),
186 path,
187 content,
188 mode,
189 create_dirs,
190 }
191 }
192
193 pub fn ping() -> Self {
195 Self::Ping {
196 id: Uuid::new_v4(),
197 timestamp: std::time::SystemTime::now()
198 .duration_since(std::time::UNIX_EPOCH)
199 .unwrap_or_default()
200 .as_secs(),
201 }
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub enum Response {
208 ProcessResult {
210 request_id: Uuid,
212 exit_code: i32,
214 stdout: Bytes,
216 stderr: Bytes,
218 duration_ms: u64,
220 },
221
222 FileContent {
224 request_id: Uuid,
226 content: Bytes,
228 metadata: FileMetadata,
230 },
231
232 FilePutResult {
234 request_id: Uuid,
236 bytes_written: u64,
238 },
239
240 DirListing {
242 request_id: Uuid,
244 entries: Vec<DirEntry>,
246 },
247
248 WasmResult {
250 request_id: Uuid,
252 output: Bytes,
254 duration_ms: u64,
256 },
257
258 JsonResult {
260 request_id: Uuid,
262 result: Bytes,
264 },
265
266 Pong {
268 request_id: Uuid,
270 timestamp: u64,
272 response_timestamp: u64,
274 },
275
276 PtyResult {
278 request_id: Uuid,
280 exit_code: i32,
282 output: Bytes,
284 duration_ms: u64,
286 },
287
288 Error {
290 request_id: Uuid,
292 error: ErrorDetails,
294 },
295}
296
297impl Response {
298 pub fn request_id(&self) -> Uuid {
300 match self {
301 Self::ProcessResult { request_id, .. } => *request_id,
302 Self::FileContent { request_id, .. } => *request_id,
303 Self::FilePutResult { request_id, .. } => *request_id,
304 Self::DirListing { request_id, .. } => *request_id,
305 Self::WasmResult { request_id, .. } => *request_id,
306 Self::JsonResult { request_id, .. } => *request_id,
307 Self::Pong { request_id, .. } => *request_id,
308 Self::PtyResult { request_id, .. } => *request_id,
309 Self::Error { request_id, .. } => *request_id,
310 }
311 }
312
313 pub fn error(request_id: Uuid, error: ErrorDetails) -> Self {
315 Self::Error { request_id, error }
316 }
317
318 pub fn pong(request_id: Uuid, timestamp: u64) -> Self {
320 Self::Pong {
321 request_id,
322 timestamp,
323 response_timestamp: std::time::SystemTime::now()
324 .duration_since(std::time::UNIX_EPOCH)
325 .unwrap_or_default()
326 .as_secs(),
327 }
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct FileMetadata {
334 pub size: u64,
336 pub mode: u32,
338 pub modified: u64,
340 pub is_dir: bool,
342 pub is_symlink: bool,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct DirEntry {
349 pub name: String,
351 pub path: PathBuf,
353 pub metadata: FileMetadata,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ErrorDetails {
360 pub code: ErrorCode,
362 pub message: String,
364 pub context: HashMap<String, String>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct PrivilegeEscalation {
371 pub method: PrivilegeMethod,
373 pub credentials: Option<Credentials>,
375 pub prompt_patterns: Vec<String>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub enum PrivilegeMethod {
382 Sudo,
384 Su,
386 Doas,
388 Custom(String),
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct Credentials {
395 pub username: Option<String>,
397 pub password: Option<String>,
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403pub enum ErrorCode {
404 InvalidRequest,
406 FileNotFound,
408 PermissionDenied,
410 ProcessFailed,
412 WasmFailed,
414 Timeout,
416 InternalError,
418 Unsupported,
420 ResourceExhausted,
422 PrivilegeEscalationFailed,
424}
425
426impl ErrorDetails {
427 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
429 Self {
430 code,
431 message: message.into(),
432 context: HashMap::new(),
433 }
434 }
435
436 pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
438 self.context.insert(key.into(), value.into());
439 self
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use proptest::prelude::*;
447
448 #[test]
449 fn test_request_creation() {
450 let req = Request::process_exec(
451 vec!["echo".to_string(), "hello".to_string()],
452 HashMap::new(),
453 None,
454 None,
455 Some(30),
456 );
457
458 match req {
459 Request::ProcessExec { command, timeout, .. } => {
460 assert_eq!(command, vec!["echo", "hello"]);
461 assert_eq!(timeout, Some(30));
462 }
463 _ => panic!("Expected ProcessExec request"),
464 }
465 }
466
467 #[test]
468 fn test_response_creation() {
469 let request_id = Uuid::new_v4();
470 let resp = Response::error(
471 request_id,
472 ErrorDetails::new(ErrorCode::FileNotFound, "File not found"),
473 );
474
475 match resp {
476 Response::Error { request_id: resp_id, error } => {
477 assert_eq!(resp_id, request_id);
478 assert_eq!(error.code, ErrorCode::FileNotFound);
479 assert_eq!(error.message, "File not found");
480 }
481 _ => panic!("Expected Error response"),
482 }
483 }
484
485 #[test]
486 fn test_message_request_id() {
487 let req = Request::ping();
488 let req_id = req.id();
489 let msg = Message::request(req);
490
491 assert_eq!(msg.request_id(), Some(req_id));
492 }
493
494 #[test]
495 fn test_error_details_with_context() {
496 let error = ErrorDetails::new(ErrorCode::ProcessFailed, "Command failed")
497 .with_context("command", "ls")
498 .with_context("exit_code", "1");
499
500 assert_eq!(error.code, ErrorCode::ProcessFailed);
501 assert_eq!(error.message, "Command failed");
502 assert_eq!(error.context.get("command"), Some(&"ls".to_string()));
503 assert_eq!(error.context.get("exit_code"), Some(&"1".to_string()));
504 }
505
506 #[test]
507 fn test_message_serialization() {
508 let req = Request::ping();
509 let msg = Message::request(req);
510
511 let serialized = rmp_serde::to_vec(&msg).unwrap();
512 let deserialized: Message = rmp_serde::from_slice(&serialized).unwrap();
513
514 assert_eq!(msg.request_id(), deserialized.request_id());
515 }
516
517 proptest! {
518 #[test]
519 fn test_request_id_consistency(
520 command in prop::collection::vec("[a-zA-Z0-9]+", 1..5),
521 timeout in prop::option::of(1u64..3600)
522 ) {
523 let req = Request::process_exec(
524 command,
525 HashMap::new(),
526 None,
527 None,
528 timeout,
529 );
530
531 let id1 = req.id();
532 let id2 = req.id();
533 prop_assert_eq!(id1, id2);
534 }
535 }
536}