Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/buzz-dev-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl DevMcp {

#[tool(
name = "shell",
description = "Run a shell command (bash by default; set `BUZZ_SHELL` to use cmd, PowerShell, or another shell). Ephemeral process per call. Output tail-truncated to ~8KB for the LLM; full output (first 10MB) saved to artifact file. timeout_ms defaults to 120000 (2 min) if omitted; capped at 600000 (10 min). For long-running commands (git push with hooks, cargo build, test suites), use 300000+. On PATH: rg (prefer over grep; flags: -n -i -l -g <glob> -C <n> --files), tree (flags: -d <depth>; shows line counts), and buzz (Buzz relay CLI — run buzz --help for commands)."
description = "Run a shell command (bash by default; set `BUZZ_SHELL` to use cmd, PowerShell, or another shell). Ephemeral process per call. Output tail-truncated to ~8KB for the LLM; full output (first 10MB) saved to artifact file. timeout_ms defaults to 120000 (2 min) if omitted; capped at 1,200,000 (20 min). For long-running commands (git push with hooks, cargo build, test suites), use 300000+. On PATH: rg (prefer over grep; flags: -n -i -l -g <glob> -C <n> --files), tree (flags: -d <depth>; shows line counts), and buzz (Buzz relay CLI — run buzz --help for commands)."
)]
async fn shell(
&self,
Expand Down
22 changes: 16 additions & 6 deletions crates/buzz-dev-mcp/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use tokio::process::Command;
use tokio_util::sync::CancellationToken;

const DEFAULT_TIMEOUT_MS: u64 = 120_000;
const MAX_TIMEOUT_MS: u64 = 600_000;
const MAX_TIMEOUT_MS: u64 = 1_200_000;
const MAX_COMMAND_BYTES: usize = 1_000_000;
const CAPTURE_CAP: usize = 10 * 1024 * 1024;
const MAX_BYTES: usize = 50 * 1024;
Expand Down Expand Up @@ -121,12 +121,16 @@ pub struct ShellParams {
pub command: String,
#[serde(default)]
pub workdir: Option<String>,
/// Defaults to 120000 ms (2 min) if omitted; capped at 600000 ms (10 min).
/// Defaults to 120000 ms (2 min) if omitted; capped at 1,200,000 ms (20 min).
/// For long-running commands (git push with hooks, cargo build, test suites), use 300000+.
#[serde(default)]
pub timeout_ms: Option<u64>,
}

fn effective_timeout_ms(requested: Option<u64>) -> u64 {
requested.unwrap_or(DEFAULT_TIMEOUT_MS).min(MAX_TIMEOUT_MS)
}

pub async fn run(
state: &SharedState,
p: ShellParams,
Expand All @@ -138,10 +142,7 @@ pub async fn run(
None,
));
}
let timeout_ms = p
.timeout_ms
.unwrap_or(DEFAULT_TIMEOUT_MS)
.min(MAX_TIMEOUT_MS);
let timeout_ms = effective_timeout_ms(p.timeout_ms);
let workdir: PathBuf = p
.workdir
.as_deref()
Expand Down Expand Up @@ -1002,6 +1003,15 @@ mod tests {
serde_json::from_str(&text).expect("json")
}

#[test]
fn timeout_bounds_preserve_default_and_cap_requests_at_twenty_minutes() {
assert_eq!(effective_timeout_ms(None), 120_000);
assert_eq!(effective_timeout_ms(Some(120_000)), 120_000);
assert_eq!(effective_timeout_ms(Some(1_200_000)), 1_200_000);
assert_eq!(effective_timeout_ms(Some(1_200_001)), 1_200_000);
assert_eq!(effective_timeout_ms(Some(u64::MAX)), 1_200_000);
}

#[tokio::test(flavor = "current_thread")]
async fn basic_echo() {
let dir = tempdir().expect("tempdir");
Expand Down
Loading