magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
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)
    }
}