Files
zesdex/apps/infrastructure/src/tools/bash_tools.rs
T

107 lines
3.2 KiB
Rust
Raw Normal View History

//! Background bash process output and kill tools.
//!
//! These tools allow the agent to inspect the output of a background shell job
//! (`BashOutput`) and to terminate a running background job (`BashKill`).
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{info, instrument, warn};
use crate::tools::{arg_str, Tool, ToolCtx};
/// Get the output of a background bash job by ID.
///
/// Flow: look up `{session_dir}/bash-outputs/{job_id}` → read content back.
/// Path traversal in `job_id` is explicitly rejected.
pub struct BashOutput;
impl Tool for BashOutput {
fn name(&self) -> &'static str {
"bash_output"
}
fn description(&self) -> &'static str {
"Get the output of a background bash job by ID"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"job_id": {
"type": "string",
"description": "Background job ID"
}
},
"required": ["job_id"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let job_id = arg_str(args, "job_id")?;
info!("Getting output for job: {job_id}");
// Prevent path traversal
if job_id.contains('/') || job_id.contains('\\') || job_id.contains("..") {
anyhow::bail!("invalid job_id '{job_id}': must not contain path separators");
}
// Read from the session's bash output directory
let output_dir = ctx.session_dir.join("bash-outputs");
let output_file = output_dir.join(&job_id);
if output_file.exists() {
let content = std::fs::read_to_string(&output_file)
.unwrap_or_else(|e| format!("Error reading output: {e}"));
Ok(format!("Output for job '{job_id}':\n{content}"))
} else {
Ok(format!(
"No output found for job '{job_id}'. The job may still be running."
))
}
}
}
/// Kill a background bash job by ID.
///
/// Flow: parse job_id → look up in the global bash controller → cancel the job.
pub struct BashKill;
impl Tool for BashKill {
fn name(&self) -> &'static str {
"bash_kill"
}
fn description(&self) -> &'static str {
"Kill a background bash job by ID"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"job_id": {
"type": "string",
"description": "Background job ID to kill"
}
},
"required": ["job_id"]
})
}
#[instrument(skip(self, _ctx, args))]
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let job_id = crate::tools::arg_str(args, "job_id")?;
info!("bash_kill called for job: {job_id}");
if crate::bgbash::control::bash_control().cancel(&job_id) {
info!(job = %job_id, "background job killed");
Ok(format!("Killed background job '{job_id}'"))
} else {
warn!(job = %job_id, "no active background job found");
anyhow::bail!("no active background job found with ID '{job_id}'")
}
}
}