use super::{
ToolResult, ToolResultDisplay, ToolRuntime,
args::{BASH_DEFAULT_TIMEOUT_SECS, BASH_STDERR_MAX_BYTES, BASH_STDOUT_MAX_BYTES, BashArgs},
contract::{metadata_key as meta, tool_name},
process::{BoundedChildProcessLimits, run_bounded_child_process},
};
use crate::{
cancellation::AgentCancellation,
shell::runtime::{self, ShellEnvPolicy, ShellStdin},
};
use serde_json::json;
use std::time::Duration;
impl ToolRuntime {
pub(super) fn bash(
&self,
args: BashArgs,
cancellation: &AgentCancellation,
) -> anyhow::Result<ToolResult> {
args.validate()?;
let command = args
.effective_command()
.expect("validated command")
.to_string();
runtime::preflight_bash_cwd_scope(
&command,
self.bash_absolute_paths,
self.bash_shell_expansion,
)?;
let timeout = Duration::from_secs(args.timeout.unwrap_or(BASH_DEFAULT_TIMEOUT_SECS));
let child = runtime::spawn_platform_shell(
&command,
&self.cwd,
ShellStdin::Null,
ShellEnvPolicy::Ambient,
)?;
let file_tracking = self.bash_file_tracking.as_ref().map(|client| {
super::bash_file_tracking::PendingFileTracking::start(
&command,
&self.cwd,
cancellation,
client.clone(),
)
});
let output = run_bounded_child_process(
child,
BoundedChildProcessLimits {
stdout_max_bytes: BASH_STDOUT_MAX_BYTES,
stderr_max_bytes: BASH_STDERR_MAX_BYTES,
timeout,
poll_interval: Duration::from_millis(20),
},
cancellation,
)?;
let stdout = output.stdout;
let stderr = output.stderr;
let cleanup_warning = output.cleanup_warning;
let stdout_truncated = output.stdout_truncated;
let stderr_truncated = output.stderr_truncated;
let exit_code = if output.timed_out {
None
} else {
output.status.as_ref().and_then(|status| status.code())
};
let mut content = format!("stdout:\n{stdout}\nstderr:\n{stderr}");
if stdout_truncated || stderr_truncated {
content.push_str(&format!(
"\n[output truncated: stdout limit {BASH_STDOUT_MAX_BYTES} bytes, stderr limit {BASH_STDERR_MAX_BYTES} bytes]"
));
}
if let Some(cleanup_warning) = &cleanup_warning {
content.push_str("\n[");
content.push_str(cleanup_warning);
content.push(']');
}
let mut result = ToolResult {
tool_name: tool_name::BASH.to_string(),
success: output
.status
.as_ref()
.is_some_and(|status| status.success())
&& !output.timed_out
&& !stdout_truncated
&& !stderr_truncated
&& cleanup_warning.is_none(),
content,
metadata: json!({(meta::EXIT_CODE): exit_code, (meta::TIMED_OUT): output.timed_out, (meta::STDOUT): stdout, (meta::STDERR): stderr, (meta::STDOUT_TRUNCATED): stdout_truncated, (meta::STDERR_TRUNCATED): stderr_truncated, (meta::STDOUT_LIMIT_BYTES): BASH_STDOUT_MAX_BYTES, (meta::STDERR_LIMIT_BYTES): BASH_STDERR_MAX_BYTES, (meta::CLEANUP_WARNING): cleanup_warning}),
display: ToolResultDisplay::default(),
};
if let Some(tracking) = file_tracking {
let outcome = if exit_code == Some(0) && !output.timed_out {
tracking
.map(|tracking| tracking.finish(cancellation))
.unwrap_or_else(|outcome| outcome)
} else {
super::bash_file_tracking::FileTrackingOutcome::omitted("command_failed")
};
result.metadata["bash_file_tracking"] =
json!({"status": outcome.status, "source": outcome.source});
if !outcome.activity.is_empty() {
result.metadata["inferred_file_activity"] = json!(outcome.activity);
}
}
Ok(result)
}
}