Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/src/executor.rs

5.1 KiB, 1 run

created by r2519314175:945, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Command execution backend for the agent's shell tool.
2//!
3//! `Executor` is a pluggable boundary (plan D4): the `Local` variant
4//! runs commands directly under the Daimond process's user, in a given
5//! working directory, with a timeout. It is a *run location*, not a
6//! security sandbox — appropriate for the trusted, self-hosted
7//! environment (plan D0). A future `Remote` variant can offload
8//! execution to another host without changing callers.
9
10use oxedyne_fe2o3_core::prelude::*;
11
12use std::path::Path;
13#[cfg(not(target_arch = "wasm32"))]
14use std::time::Duration;
15
16
17/// Where and how the agent runs shell commands.
18#[derive(Clone, Debug)]
19pub enum Executor {
20 /// Run locally under the Daimond process's user, capped by `timeout`.
21 #[cfg(not(target_arch = "wasm32"))]
22 Local { timeout: Duration },
23 /// Run inside the browser (wasm32). In-browser shell execution is
24 /// not yet wired up, so a command escalates rather than running.
25 // TODO(wasm-exec): route shell commands to an in-browser sandbox or
26 // a server round-trip once the browser tool surface is built.
27 #[cfg(target_arch = "wasm32")]
28 Wasm,
29}
30
31/// The captured result of a command.
32#[derive(Clone, Debug)]
33pub struct CommandOutput {
34 pub stdout: String,
35 pub stderr: String,
36 pub exit_code: i32,
37 pub timed_out: bool,
38}
39
40impl Executor {
41
42 /// A local executor with a sensible default timeout.
43 #[cfg(not(target_arch = "wasm32"))]
44 pub fn local_default() -> Self {
45 Self::Local { timeout: Duration::from_secs(120) }
46 }
47
48 /// Run `command` via `sh -c` in `cwd`, capturing stdout/stderr.
49 ///
50 /// On timeout the child is killed (via `kill_on_drop`) and a
51 /// `timed_out` result is returned rather than an error. On wasm32
52 /// there is no local process backend, so this escalates.
53 pub async fn run(&self, command: &str, cwd: &Path) -> Outcome<CommandOutput> {
54 match self {
55 #[cfg(not(target_arch = "wasm32"))]
56 Self::Local { timeout } => {
57 use tokio::process::Command;
58 let mut cmd = Command::new("sh");
59 cmd.arg("-c").arg(command)
60 .current_dir(cwd)
61 .stdin(std::process::Stdio::null())
62 .stdout(std::process::Stdio::piped())
63 .stderr(std::process::Stdio::piped())
64 .kill_on_drop(true);
65 let child = res!(cmd.spawn()
66 .map_err(|e| err!(e, "Executor: failed to spawn command."; IO, Init)));
67 match tokio::time::timeout(*timeout, child.wait_with_output()).await {
68 Ok(Ok(out)) => Ok(CommandOutput {
69 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
70 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
71 exit_code: out.status.code().unwrap_or(-1),
72 timed_out: false,
73 }),
74 Ok(Err(e)) => Err(err!(e, "Executor: waiting on command failed."; IO)),
75 Err(_) => Ok(CommandOutput {
76 stdout: String::new(),
77 stderr: fmt!("Command timed out after {} seconds.", timeout.as_secs()),
78 exit_code: -1,
79 timed_out: true,
80 }),
81 }
82 }
83 #[cfg(target_arch = "wasm32")]
84 Self::Wasm => {
85 // In-browser shell execution is not yet available; the
86 // caller must escalate this to a server round-trip.
87 let _ = (command, cwd);
88 Err(err!(
89 "Executor: in-browser shell execution is not yet \
90 supported; escalation required.";
91 Unimplemented))
92 }
93 }
94 }
95}
96
97
98// ┌───────────────────────────────────────────────────────────────┐
99// │ Tests │
100// └───────────────────────────────────────────────────────────────┘
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[tokio::test]
107 async fn test_run_echo() {
108 let ex = Executor::local_default();
109 let out = ex.run("echo hello", Path::new("/tmp")).await.expect("run");
110 assert_eq!(out.stdout.trim(), "hello");
111 assert_eq!(out.exit_code, 0);
112 assert!(!out.timed_out);
113 }
114
115 #[tokio::test]
116 async fn test_run_exit_code() {
117 let ex = Executor::local_default();
118 let out = ex.run("exit 3", Path::new("/tmp")).await.expect("run");
119 assert_eq!(out.exit_code, 3);
120 }
121
122 #[tokio::test]
123 async fn test_run_timeout() {
124 let ex = Executor::Local { timeout: Duration::from_millis(200) };
125 let out = ex.run("sleep 5", Path::new("/tmp")).await.expect("run");
126 assert!(out.timed_out);
127 }
128}