//! 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 { 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 { 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}'") } } }