@mmmbuto/masix 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +18 -14
  2. package/install.js +53 -27
  3. package/package.json +4 -3
  4. package/packages/plugin-base/codex-backend/0.1.4/SHA256SUMS +3 -0
  5. package/packages/plugin-base/codex-backend/0.1.4/codex-backend-android-aarch64-termux.pkg +0 -0
  6. package/packages/plugin-base/codex-backend/0.1.4/codex-backend-linux-x86_64.pkg +0 -0
  7. package/packages/plugin-base/codex-backend/0.1.4/codex-backend-macos-aarch64.pkg +0 -0
  8. package/packages/plugin-base/codex-backend/0.1.4/manifest.json +33 -0
  9. package/packages/plugin-base/codex-backend/CHANGELOG.md +17 -0
  10. package/packages/plugin-base/codex-backend/README.md +33 -0
  11. package/packages/plugin-base/codex-backend/source/Cargo.toml +25 -0
  12. package/packages/plugin-base/codex-backend/source/README-PACKAGE.txt +54 -0
  13. package/packages/plugin-base/codex-backend/source/plugin.manifest.json +103 -0
  14. package/packages/plugin-base/codex-backend/source/src/error.rs +60 -0
  15. package/packages/plugin-base/codex-backend/source/src/exec.rs +436 -0
  16. package/packages/plugin-base/codex-backend/source/src/http_backend.rs +1198 -0
  17. package/packages/plugin-base/codex-backend/source/src/lib.rs +328 -0
  18. package/packages/plugin-base/codex-backend/source/src/patch.rs +767 -0
  19. package/packages/plugin-base/codex-backend/source/src/policy.rs +297 -0
  20. package/packages/plugin-base/codex-backend/source/src/tools.rs +72 -0
  21. package/packages/plugin-base/codex-backend/source/src/workspace.rs +433 -0
  22. package/packages/plugin-base/codex-tools/0.1.3/SHA256SUMS +3 -0
  23. package/packages/plugin-base/codex-tools/0.1.3/codex-tools-android-aarch64-termux.pkg +0 -0
  24. package/packages/plugin-base/codex-tools/0.1.3/codex-tools-linux-x86_64.pkg +0 -0
  25. package/packages/plugin-base/codex-tools/0.1.3/codex-tools-macos-aarch64.pkg +0 -0
  26. package/packages/plugin-base/codex-tools/0.1.3/manifest.json +33 -0
  27. package/packages/plugin-base/codex-tools/CHANGELOG.md +17 -0
  28. package/packages/plugin-base/codex-tools/README.md +33 -0
  29. package/packages/plugin-base/codex-tools/source/Cargo.toml +23 -0
  30. package/packages/plugin-base/codex-tools/source/plugin.manifest.json +124 -0
  31. package/packages/plugin-base/codex-tools/source/src/main.rs +995 -0
  32. package/packages/plugin-base/discovery/0.2.4/SHA256SUMS +3 -0
  33. package/packages/plugin-base/discovery/0.2.4/discovery-android-aarch64-termux.pkg +0 -0
  34. package/packages/plugin-base/discovery/0.2.4/discovery-linux-x86_64.pkg +0 -0
  35. package/packages/plugin-base/discovery/0.2.4/discovery-macos-aarch64.pkg +0 -0
  36. package/packages/plugin-base/discovery/0.2.4/manifest.json +31 -0
  37. package/packages/plugin-base/discovery/CHANGELOG.md +17 -0
  38. package/packages/plugin-base/discovery/README.md +48 -0
  39. package/packages/plugin-base/discovery/source/Cargo.toml +14 -0
  40. package/packages/plugin-base/discovery/source/plugin.manifest.json +30 -0
  41. package/packages/plugin-base/discovery/source/src/main.rs +2570 -0
  42. package/prebuilt/masix +0 -0
@@ -0,0 +1,436 @@
1
+ //! Command execution with safety bounds
2
+ //!
3
+ //! Provides secure one-shot command execution with:
4
+ //! - Command allowlist/denylist
5
+ //! - Timeout enforcement
6
+ //! - Output size bounds
7
+ //! - Working directory validation
8
+
9
+ use crate::{CodingError, ExecutionProfile, ResolvedWorkspace};
10
+ use std::path::PathBuf;
11
+ use std::process::Stdio;
12
+ use std::time::{Duration, Instant};
13
+ use tokio::process::Command;
14
+
15
+ /// Maximum output size (1MB default)
16
+ pub const DEFAULT_MAX_OUTPUT_BYTES: usize = 1024 * 1024;
17
+
18
+ /// Default timeout (60 seconds for exec)
19
+ pub const DEFAULT_EXEC_TIMEOUT_SECS: u64 = 60;
20
+
21
+ /// Maximum timeout allowed (5 minutes)
22
+ pub const MAX_EXEC_TIMEOUT_SECS: u64 = 300;
23
+
24
+ /// Truncation marker for output
25
+ const TRUNCATION_MARKER: &str = "\n...[OUTPUT TRUNCATED]";
26
+
27
+ /// Default command allowlist (safe commands)
28
+ pub const DEFAULT_COMMAND_ALLOWLIST: &[&str] = &[
29
+ "ls", "cat", "head", "tail", "wc", "grep", "find", "git", "pwd", "echo", "which", "dirname",
30
+ "basename", "realpath", "readlink", "stat", "file", "tree", "du", "diff", "sort", "uniq",
31
+ "cut", "awk", "sed", "tr", "xargs", "env", "printenv", "date", "uname", "id", "whoami",
32
+ "hostname", "cargo", "rustc", "rustup", "npm", "node", "yarn", "pnpm", "python", "python3",
33
+ "pip", "pip3", "go", "gofmt", "make", "cmake", "gcc", "g++", "clang", "clang++", "mvn",
34
+ "gradle", "dotnet", "ruby", "gem", "bundle", "php", "composer", "perl", "cargo", "rustfmt",
35
+ "clippy", "rg", "fd", "bat", "exa", "jq", "yq", "tomlq", "sqlite3",
36
+ ];
37
+
38
+ /// Commands that are always denied (dangerous operations)
39
+ pub const COMMAND_DENYLIST: &[&str] = &[
40
+ "rm",
41
+ "rmdir",
42
+ "dd",
43
+ "mkfs",
44
+ "fdisk",
45
+ "parted",
46
+ "shutdown",
47
+ "reboot",
48
+ "halt",
49
+ "poweroff",
50
+ "init",
51
+ "systemctl",
52
+ "service",
53
+ "apt",
54
+ "apt-get",
55
+ "yum",
56
+ "dnf",
57
+ "pacman",
58
+ "brew",
59
+ "snap",
60
+ "flatpak",
61
+ "chown",
62
+ "chmod",
63
+ "chgrp",
64
+ "passwd",
65
+ "su",
66
+ "sudo",
67
+ "doas",
68
+ "pkexec",
69
+ "gksudo",
70
+ "kdesu",
71
+ "nc",
72
+ "ncat",
73
+ "netcat",
74
+ "telnet",
75
+ "ssh",
76
+ "scp",
77
+ "sftp",
78
+ "rsync",
79
+ "curl",
80
+ "wget",
81
+ "aria2c",
82
+ "tcpdump",
83
+ "wireshark",
84
+ "tshark",
85
+ "nmap",
86
+ "masscan",
87
+ "iptables",
88
+ "ip6tables",
89
+ "nft",
90
+ "ufw",
91
+ "firewall-cmd",
92
+ ];
93
+
94
+ /// Execution request
95
+ #[derive(Debug, Clone)]
96
+ pub struct ExecRequest {
97
+ /// Command to execute (binary name or path)
98
+ pub command: String,
99
+ /// Command arguments
100
+ pub args: Vec<String>,
101
+ /// Working directory (must be within workspace)
102
+ pub cwd: Option<PathBuf>,
103
+ /// Timeout in seconds
104
+ pub timeout_secs: u64,
105
+ /// Maximum output bytes
106
+ pub max_output_bytes: usize,
107
+ /// Whether to check allowlist
108
+ pub check_allowlist: bool,
109
+ }
110
+
111
+ impl Default for ExecRequest {
112
+ fn default() -> Self {
113
+ Self {
114
+ command: String::new(),
115
+ args: Vec::new(),
116
+ cwd: None,
117
+ timeout_secs: DEFAULT_EXEC_TIMEOUT_SECS,
118
+ max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
119
+ check_allowlist: true,
120
+ }
121
+ }
122
+ }
123
+
124
+ impl ExecRequest {
125
+ /// Create a new exec request
126
+ pub fn new(command: impl Into<String>) -> Self {
127
+ Self {
128
+ command: command.into(),
129
+ ..Default::default()
130
+ }
131
+ }
132
+
133
+ /// Add an argument
134
+ pub fn arg(mut self, arg: impl Into<String>) -> Self {
135
+ self.args.push(arg.into());
136
+ self
137
+ }
138
+
139
+ /// Add multiple arguments
140
+ pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
141
+ self.args.extend(args.into_iter().map(Into::into));
142
+ self
143
+ }
144
+
145
+ /// Set working directory
146
+ pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
147
+ self.cwd = Some(cwd.into());
148
+ self
149
+ }
150
+
151
+ /// Set timeout
152
+ pub fn timeout_secs(mut self, secs: u64) -> Self {
153
+ self.timeout_secs = secs.min(MAX_EXEC_TIMEOUT_SECS);
154
+ self
155
+ }
156
+
157
+ /// Set max output bytes
158
+ pub fn max_output_bytes(mut self, bytes: usize) -> Self {
159
+ self.max_output_bytes = bytes;
160
+ self
161
+ }
162
+
163
+ /// Disable allowlist checking (admin-only)
164
+ pub fn bypass_allowlist(mut self) -> Self {
165
+ self.check_allowlist = false;
166
+ self
167
+ }
168
+ }
169
+
170
+ /// Execution result
171
+ #[derive(Debug, Clone)]
172
+ pub struct ExecResult {
173
+ /// Exit code (None if killed/timeout)
174
+ pub exit_code: Option<i32>,
175
+ /// Standard output
176
+ pub stdout: String,
177
+ /// Standard error
178
+ pub stderr: String,
179
+ /// Whether the command was killed due to timeout
180
+ pub timed_out: bool,
181
+ /// Whether output was truncated
182
+ pub output_truncated: bool,
183
+ /// Execution duration
184
+ pub duration: Duration,
185
+ }
186
+
187
+ impl ExecResult {
188
+ /// Check if execution was successful
189
+ pub fn success(&self) -> bool {
190
+ self.exit_code == Some(0)
191
+ }
192
+ }
193
+
194
+ /// Command executor with safety bounds
195
+ #[derive(Debug, Clone)]
196
+ pub struct CommandExecutor {
197
+ /// Workspace for path validation
198
+ workspace: ResolvedWorkspace,
199
+ /// Additional allowed commands
200
+ extra_allowed: Vec<String>,
201
+ /// Execution profile
202
+ profile: ExecutionProfile,
203
+ }
204
+
205
+ impl CommandExecutor {
206
+ /// Create a new executor for a workspace
207
+ pub fn new(workspace: ResolvedWorkspace) -> Self {
208
+ Self {
209
+ workspace,
210
+ extra_allowed: Vec::new(),
211
+ profile: ExecutionProfile::WorkspaceLocked,
212
+ }
213
+ }
214
+
215
+ /// Set execution profile
216
+ pub fn with_profile(mut self, profile: ExecutionProfile) -> Self {
217
+ self.profile = profile;
218
+ self
219
+ }
220
+
221
+ /// Add an allowed command
222
+ pub fn allow_command(mut self, cmd: impl Into<String>) -> Self {
223
+ self.extra_allowed.push(cmd.into());
224
+ self
225
+ }
226
+
227
+ /// Check if a command is allowed
228
+ pub fn is_command_allowed(&self, command: &str) -> Result<(), CodingError> {
229
+ let base_name = base_command_name(command);
230
+
231
+ // Always check denylist
232
+ if COMMAND_DENYLIST.contains(&base_name.as_str()) {
233
+ return Err(CodingError::PolicyError(format!(
234
+ "Command '{}' is in denylist",
235
+ base_name
236
+ )));
237
+ }
238
+
239
+ // If allowlist checking is disabled (SystemUnlocked profile), allow
240
+ if self.profile == ExecutionProfile::SystemUnlocked {
241
+ return Ok(());
242
+ }
243
+
244
+ // Check if in default allowlist
245
+ if DEFAULT_COMMAND_ALLOWLIST.contains(&base_name.as_str()) {
246
+ return Ok(());
247
+ }
248
+
249
+ // Check if in extra allowed
250
+ if self.extra_allowed.iter().any(|c| c == &base_name) {
251
+ return Ok(());
252
+ }
253
+
254
+ Err(CodingError::PolicyError(format!(
255
+ "Command '{}' not in allowlist",
256
+ base_name
257
+ )))
258
+ }
259
+
260
+ /// Execute a command
261
+ pub async fn execute(&self, request: ExecRequest) -> Result<ExecResult, CodingError> {
262
+ let start_time = Instant::now();
263
+
264
+ // Validate command policy
265
+ if request.check_allowlist {
266
+ self.is_command_allowed(&request.command)?;
267
+ } else {
268
+ let base_name = base_command_name(&request.command);
269
+ if COMMAND_DENYLIST.contains(&base_name.as_str()) {
270
+ return Err(CodingError::PolicyError(format!(
271
+ "Command '{}' is in denylist",
272
+ base_name
273
+ )));
274
+ }
275
+ if self.profile != ExecutionProfile::SystemUnlocked {
276
+ return Err(CodingError::PolicyError(
277
+ "Allowlist bypass requires system_unlocked profile".into(),
278
+ ));
279
+ }
280
+ }
281
+
282
+ // Validate timeout
283
+ let timeout = Duration::from_secs(request.timeout_secs.min(MAX_EXEC_TIMEOUT_SECS));
284
+
285
+ // Validate working directory
286
+ let cwd = if let Some(ref cwd) = request.cwd {
287
+ self.workspace.is_path_allowed(cwd)?
288
+ } else {
289
+ self.workspace.root.clone()
290
+ };
291
+
292
+ // Build command
293
+ let mut cmd = Command::new(&request.command);
294
+ cmd.args(&request.args)
295
+ .current_dir(&cwd)
296
+ .stdin(Stdio::null())
297
+ .stdout(Stdio::piped())
298
+ .stderr(Stdio::piped());
299
+
300
+ // Kill process if dropped during timeout or cancellation.
301
+ cmd.kill_on_drop(true);
302
+
303
+ // Spawn process
304
+ let child = cmd.spawn().map_err(|e| {
305
+ CodingError::ToolError(format!("Failed to spawn '{}': {}", request.command, e))
306
+ })?;
307
+
308
+ // Wait for completion with timeout while collecting both stdout/stderr.
309
+ let result = tokio::time::timeout(timeout, child.wait_with_output()).await;
310
+
311
+ let (exit_code, timed_out, stdout_raw, stderr_raw) = match result {
312
+ Ok(Ok(output)) => (output.status.code(), false, output.stdout, output.stderr),
313
+ Ok(Err(e)) => {
314
+ return Err(CodingError::ToolError(format!("Process error: {}", e)));
315
+ }
316
+ Err(_) => (None, true, Vec::new(), Vec::new()),
317
+ };
318
+
319
+ let (stdout, stdout_truncated) = truncate_bytes(&stdout_raw, request.max_output_bytes);
320
+ let (stderr, stderr_truncated) = truncate_bytes(&stderr_raw, request.max_output_bytes);
321
+
322
+ Ok(ExecResult {
323
+ exit_code,
324
+ stdout,
325
+ stderr,
326
+ timed_out,
327
+ output_truncated: stdout_truncated || stderr_truncated,
328
+ duration: start_time.elapsed(),
329
+ })
330
+ }
331
+ }
332
+
333
+ fn base_command_name(command: &str) -> String {
334
+ PathBuf::from(command)
335
+ .file_name()
336
+ .and_then(|n| n.to_str())
337
+ .unwrap_or(command)
338
+ .to_string()
339
+ }
340
+
341
+ fn truncate_bytes(bytes: &[u8], max_bytes: usize) -> (String, bool) {
342
+ if bytes.len() <= max_bytes {
343
+ return (String::from_utf8_lossy(bytes).to_string(), false);
344
+ }
345
+
346
+ let truncated = String::from_utf8_lossy(&bytes[..max_bytes]).to_string();
347
+ (format!("{}{}", truncated, TRUNCATION_MARKER), true)
348
+ }
349
+
350
+ #[cfg(test)]
351
+ mod tests {
352
+ use super::*;
353
+
354
+ fn test_workspace() -> ResolvedWorkspace {
355
+ ResolvedWorkspace {
356
+ root: std::env::temp_dir(),
357
+ source: crate::workspace::WorkspaceSource::DefaultRoot,
358
+ profile: ExecutionProfile::WorkspaceLocked,
359
+ allowed_roots: Vec::new(),
360
+ }
361
+ }
362
+
363
+ #[test]
364
+ fn test_command_allowed() {
365
+ let executor = CommandExecutor::new(test_workspace());
366
+
367
+ assert!(executor.is_command_allowed("ls").is_ok());
368
+ assert!(executor.is_command_allowed("git").is_ok());
369
+ assert!(executor.is_command_allowed("cargo").is_ok());
370
+ }
371
+
372
+ #[test]
373
+ fn test_command_denied() {
374
+ let executor = CommandExecutor::new(test_workspace());
375
+
376
+ assert!(executor.is_command_allowed("rm").is_err());
377
+ assert!(executor.is_command_allowed("sudo").is_err());
378
+ assert!(executor.is_command_allowed("apt").is_err());
379
+ }
380
+
381
+ #[test]
382
+ fn test_command_not_in_allowlist() {
383
+ let executor = CommandExecutor::new(test_workspace());
384
+
385
+ // A command not in allowlist but not in denylist
386
+ let result = executor.is_command_allowed("my-custom-tool");
387
+ assert!(result.is_err());
388
+ assert!(matches!(result, Err(CodingError::PolicyError(_))));
389
+ }
390
+
391
+ #[test]
392
+ fn test_exec_request_builder() {
393
+ let req = ExecRequest::new("git")
394
+ .args(["status", "--short"])
395
+ .timeout_secs(30)
396
+ .max_output_bytes(1024);
397
+
398
+ assert_eq!(req.command, "git");
399
+ assert_eq!(req.args, vec!["status", "--short"]);
400
+ assert_eq!(req.timeout_secs, 30);
401
+ assert_eq!(req.max_output_bytes, 1024);
402
+ }
403
+
404
+ #[tokio::test]
405
+ async fn test_execute_simple_command() {
406
+ let executor = CommandExecutor::new(test_workspace());
407
+ let req = ExecRequest::new("echo").arg("hello");
408
+
409
+ let result = executor.execute(req).await;
410
+ assert!(result.is_ok());
411
+
412
+ let result = result.unwrap();
413
+ assert!(result.success());
414
+ assert!(result.stdout.contains("hello"));
415
+ }
416
+
417
+ #[tokio::test]
418
+ async fn test_execute_with_timeout() {
419
+ let executor = CommandExecutor::new(test_workspace());
420
+
421
+ // This would timeout if we used sleep, but for now test basic execution
422
+ let req = ExecRequest::new("echo").arg("test").timeout_secs(1);
423
+
424
+ let result = executor.execute(req).await;
425
+ assert!(result.is_ok());
426
+ }
427
+
428
+ #[tokio::test]
429
+ async fn test_bypass_allowlist_requires_system_unlocked() {
430
+ let executor = CommandExecutor::new(test_workspace());
431
+ let req = ExecRequest::new("my-custom-tool").bypass_allowlist();
432
+
433
+ let result = executor.execute(req).await;
434
+ assert!(matches!(result, Err(CodingError::PolicyError(_))));
435
+ }
436
+ }