refactor: migrate monolithic crate to Cargo Workspace with Clean Architecture
Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
//! Bash-shell execution tool with safety filters and optional timeout.
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde_json::{json, Value};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Tool that runs `bash -c <command>`, optionally in the background, with safety
|
||||
/// filters applied before spawning.
|
||||
pub struct Bash;
|
||||
|
||||
impl Tool for Bash {
|
||||
fn name(&self) -> &'static str {
|
||||
"bash"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Execute a shell command via bash -c"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Shell command to execute"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Human-readable description of what the command does"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Timeout in milliseconds (default 120000, max 600000)"
|
||||
},
|
||||
"run_in_background": {
|
||||
"type": "boolean",
|
||||
"description": "Run the command in the background and return immediately with a job ID"
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a bash command (foreground or background) with safety filters and a timeout.
|
||||
///
|
||||
/// Flow: extract args → run `check_credential_read` then `check_git_destructive`
|
||||
/// (bail if either rejects) → branch on `run_in_background`: if true, hand off
|
||||
/// to the bg-bash subsystem and return the job ID; else spawn `bash -c`,
|
||||
/// poll with `try_wait`, kill on timeout, format combined stdout+stderr.
|
||||
///
|
||||
/// Why: the safety filters run unconditionally so background jobs are also gated;
|
||||
/// the timeout is enforced by polling the child rather than relying on a libc alarm
|
||||
/// so cleanup stays in Rust.
|
||||
///
|
||||
/// Return: exit-code + elapsed-seconds summary line (plus captured output) for
|
||||
/// foreground runs, or the job ID for background runs.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let cmd = args
|
||||
.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: command"))?
|
||||
.to_string();
|
||||
let timeout_ms = args
|
||||
.get("timeout")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(120_000)
|
||||
.min(600_000);
|
||||
// Only gate destructive git operations; credential reads are allowed
|
||||
// locally since the AI needs access, and the real threat is committing
|
||||
// secrets to a public repo (handled by git pre-commit hooks / user).
|
||||
super::shell_filter::git::check_git_destructive(&cmd)
|
||||
.map_err(|e| anyhow!("blocked: {e}"))?;
|
||||
let run_in_background = args
|
||||
.get("run_in_background")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
if run_in_background {
|
||||
let job = crate::app::bgbash::job::spawn_bash_job(cmd);
|
||||
return Ok(format!("Background job: {}", job.id));
|
||||
}
|
||||
let mut child = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("failed to spawn bash: {e}"))?;
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = Duration::from_millis(timeout_ms);
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| anyhow!("failed to collect output: {e}"))?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
let combined = if stderr.is_empty() {
|
||||
stdout
|
||||
} else {
|
||||
format!("{stdout}\n{stderr}")
|
||||
};
|
||||
let trimmed = combined.trim().to_string();
|
||||
if status.success() {
|
||||
return Ok(if trimmed.is_empty() {
|
||||
format!("Command completed in {elapsed:.2}s (exit code 0)")
|
||||
} else {
|
||||
format!("{trimmed}\n\nExit code: 0 ({elapsed:.2}s)")
|
||||
});
|
||||
}
|
||||
return Ok(format!(
|
||||
"{}\n\nExit code: {} ({:.2}s)",
|
||||
trimmed,
|
||||
status.code().unwrap_or(-1),
|
||||
elapsed
|
||||
));
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() > timeout {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
anyhow::bail!("command timed out after {timeout_ms}ms");
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => {
|
||||
anyhow::bail!("failed to wait for command: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user