Skip to main content

agent_block_core/bridge/
sh.rs

1//! sh.* — Shell command execution.
2//!
3//! # Security
4//!
5//! Currently no restrictions on command execution — Lua scripts can run
6//! arbitrary shell commands via `sh -c`.  This is intentional during
7//! development; the trust boundary is the Lua script author.
8//!
9//! A proper security model (sandboxing, allowlists, capability-based
10//! policies, etc.) will be designed separately before production use.
11
12use mlua::prelude::*;
13use std::path::PathBuf;
14use std::time::Duration;
15
16use crate::host::HostContext;
17
18pub fn register(lua: &Lua, ctx: &HostContext) -> LuaResult<()> {
19    let sh_tbl = lua.create_table()?;
20    let default_cwd = ctx.project_root.clone();
21
22    sh_tbl.set(
23        "exec",
24        lua.create_async_function(move |lua, (cmd, opts): (String, Option<LuaTable>)| {
25            let default_cwd = default_cwd.clone();
26            async move {
27                let timeout_secs: u64 = opts
28                    .as_ref()
29                    .and_then(|t| t.get::<Option<u64>>("timeout").ok().flatten())
30                    .unwrap_or(30);
31
32                let cwd: PathBuf = opts
33                    .as_ref()
34                    .and_then(|t| t.get::<Option<String>>("cwd").ok().flatten())
35                    .map(PathBuf::from)
36                    .unwrap_or_else(|| default_cwd.clone());
37
38                let result = run_async(&cmd, &cwd, Duration::from_secs(timeout_secs)).await;
39
40                match result {
41                    Ok((code, stdout, stderr)) => {
42                        let t = lua.create_table()?;
43                        t.set("ok", true)?;
44                        t.set("code", code)?;
45                        t.set("stdout", stdout)?;
46                        t.set("stderr", stderr)?;
47                        Ok(t)
48                    }
49                    Err(e) => {
50                        let t = lua.create_table()?;
51                        t.set("ok", false)?;
52                        t.set("error", e)?;
53                        Ok(t)
54                    }
55                }
56            }
57        })?,
58    )?;
59
60    lua.globals().set("sh", sh_tbl)?;
61    Ok(())
62}
63
64async fn run_async(
65    cmd: &str,
66    cwd: &PathBuf,
67    timeout: Duration,
68) -> Result<(i32, String, String), String> {
69    let child = tokio::process::Command::new("sh")
70        .arg("-c")
71        .arg(cmd)
72        .current_dir(cwd)
73        .stdout(std::process::Stdio::piped())
74        .stderr(std::process::Stdio::piped())
75        .spawn()
76        .map_err(|e| format!("exec error: {e}"))?;
77
78    let output = tokio::time::timeout(timeout, child.wait_with_output())
79        .await
80        .map_err(|_| {
81            // Timeout expired — kill the child process.
82            // child is moved into wait_with_output, so we can't kill it here.
83            // tokio drops the child on timeout which sends SIGKILL.
84            format!("timeout after {}s", timeout.as_secs())
85        })?
86        .map_err(|e| format!("wait error: {e}"))?;
87
88    let code = output.status.code().unwrap_or(-1);
89    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
90    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
91
92    Ok((code, stdout, stderr))
93}