2026-07-12 11:28:39 +07:00
|
|
|
//! 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`.
|
|
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
use anyhow::{Result, anyhow};
|
|
|
|
|
use super::Tool;
|
|
|
|
|
use super::ToolCtx;
|
|
|
|
|
|
2026-07-12 11:28:39 +07:00
|
|
|
/// Tool: fetch buffered output from a background bash job by `job_id`.
|
2026-07-11 20:21:59 +07:00
|
|
|
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<String> {
|
|
|
|
|
let job_id = args.get("job_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow!("missing required argument: job_id"))?
|
|
|
|
|
.to_string();
|
2026-07-13 04:59:16 +07:00
|
|
|
// 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");
|
|
|
|
|
}
|
2026-07-11 20:21:59 +07:00
|
|
|
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)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 11:28:39 +07:00
|
|
|
/// Tool: terminate a running background bash job by `job_id`.
|
2026-07-11 20:21:59 +07:00
|
|
|
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<String> {
|
|
|
|
|
let job_id = args.get("job_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow!("missing required argument: job_id"))?
|
|
|
|
|
.to_string();
|
2026-07-13 04:59:16 +07:00
|
|
|
if !is_valid_job_id(&job_id) {
|
|
|
|
|
anyhow::bail!("invalid job_id format: expected UUID");
|
|
|
|
|
}
|
2026-07-11 20:21:59 +07:00
|
|
|
crate::app::bgbash::control::bash_kill(&job_id)?;
|
|
|
|
|
Ok(format!("Killed background job '{}'", job_id))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-13 04:59:16 +07:00
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
}
|