//! Tool implementations for interacting with background bash jobs: `bash_output` //! and `bash_kill`. Both take a `job_id` produced by `bash` with `run_in_background=true`. use serde_json::{json, Value}; use anyhow::{Result, anyhow}; use super::Tool; use super::ToolCtx; /// Tool: fetch buffered output from a background bash job by `job_id`. pub struct BashOutput; impl Tool for BashOutput { fn name(&self) -> &'static str { "bash_output" } fn description(&self) -> &'static str { "Retrieve output from a background bash job by job_id" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "job_id": { "type": "string", "description": "Job ID returned by bash with run_in_background=true" } }, "required": ["job_id"] }) } fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let job_id = args.get("job_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: job_id"))? .to_string(); // Validate that job_id looks like a UUID to prevent injection // into the global job registry. if !is_valid_job_id(&job_id) { anyhow::bail!("invalid job_id format: expected UUID"); } match crate::app::bgbash::control::bash_output(&job_id) { Some(lines) => Ok(lines.join("\n")), None => Ok(format!("No new output from job '{job_id}'")), } } } /// Tool: terminate a running background bash job by `job_id`. 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 job_id" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "job_id": { "type": "string", "description": "Job ID returned by bash with run_in_background=true" } }, "required": ["job_id"] }) } fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { let job_id = args.get("job_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: job_id"))? .to_string(); if !is_valid_job_id(&job_id) { anyhow::bail!("invalid job_id format: expected UUID"); } crate::app::bgbash::control::bash_kill(&job_id)?; Ok(format!("Killed background job '{job_id}'")) } } /// Validate that a `job_id` matches UUID v4 format (hex with dashes). fn is_valid_job_id(id: &str) -> bool { // UUID v4 format: 8-4-4-4-12 hex digits let parts: Vec<&str> = id.split('-').collect(); if parts.len() != 5 { return false; } parts.iter().all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_hexdigit())) && parts[0].len() == 8 && parts[1].len() == 4 && parts[2].len() == 4 && parts[3].len() == 4 && parts[4].len() == 12 }