@mmmbuto/masix 0.3.8 → 0.4.1
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.
- package/README.md +17 -15
- package/install.js +89 -26
- package/package.json +4 -3
- package/packages/plugin-base/codex-backend/0.1.4/SHA256SUMS +3 -0
- package/packages/plugin-base/codex-backend/0.1.4/codex-backend-android-aarch64-termux.pkg +0 -0
- package/packages/plugin-base/codex-backend/0.1.4/codex-backend-linux-x86_64.pkg +0 -0
- package/packages/plugin-base/codex-backend/0.1.4/codex-backend-macos-aarch64.pkg +0 -0
- package/packages/plugin-base/codex-backend/0.1.4/manifest.json +33 -0
- package/packages/plugin-base/codex-backend/CHANGELOG.md +17 -0
- package/packages/plugin-base/codex-backend/README.md +33 -0
- package/packages/plugin-base/codex-backend/source/Cargo.toml +25 -0
- package/packages/plugin-base/codex-backend/source/README-PACKAGE.txt +54 -0
- package/packages/plugin-base/codex-backend/source/plugin.manifest.json +103 -0
- package/packages/plugin-base/codex-backend/source/src/error.rs +60 -0
- package/packages/plugin-base/codex-backend/source/src/exec.rs +436 -0
- package/packages/plugin-base/codex-backend/source/src/http_backend.rs +1198 -0
- package/packages/plugin-base/codex-backend/source/src/lib.rs +328 -0
- package/packages/plugin-base/codex-backend/source/src/patch.rs +767 -0
- package/packages/plugin-base/codex-backend/source/src/policy.rs +297 -0
- package/packages/plugin-base/codex-backend/source/src/tools.rs +72 -0
- package/packages/plugin-base/codex-backend/source/src/workspace.rs +433 -0
- package/packages/plugin-base/codex-tools/0.1.3/SHA256SUMS +3 -0
- package/packages/plugin-base/codex-tools/0.1.3/codex-tools-android-aarch64-termux.pkg +0 -0
- package/packages/plugin-base/codex-tools/0.1.3/codex-tools-linux-x86_64.pkg +0 -0
- package/packages/plugin-base/codex-tools/0.1.3/codex-tools-macos-aarch64.pkg +0 -0
- package/packages/plugin-base/codex-tools/0.1.3/manifest.json +33 -0
- package/packages/plugin-base/codex-tools/CHANGELOG.md +17 -0
- package/packages/plugin-base/codex-tools/README.md +33 -0
- package/packages/plugin-base/codex-tools/source/Cargo.toml +23 -0
- package/packages/plugin-base/codex-tools/source/plugin.manifest.json +124 -0
- package/packages/plugin-base/codex-tools/source/src/main.rs +995 -0
- package/packages/plugin-base/discovery/0.2.4/SHA256SUMS +3 -0
- package/packages/plugin-base/discovery/0.2.4/discovery-android-aarch64-termux.pkg +0 -0
- package/packages/plugin-base/discovery/0.2.4/discovery-linux-x86_64.pkg +0 -0
- package/packages/plugin-base/discovery/0.2.4/discovery-macos-aarch64.pkg +0 -0
- package/packages/plugin-base/discovery/0.2.4/manifest.json +31 -0
- package/packages/plugin-base/discovery/CHANGELOG.md +17 -0
- package/packages/plugin-base/discovery/README.md +48 -0
- package/packages/plugin-base/discovery/source/Cargo.toml +14 -0
- package/packages/plugin-base/discovery/source/plugin.manifest.json +30 -0
- package/packages/plugin-base/discovery/source/src/main.rs +2570 -0
- 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
|
+
}
|