From 9e2d22418b409b5edf8d1b8787803b76a7374c4c Mon Sep 17 00:00:00 2001 From: kevin9327 Date: Sun, 30 Aug 2026 19:15:29 +0900 Subject: [PATCH] fix(tools): complete the bash shell alias in the tool codec The tool dispatcher already treats `bash`/`Bash` as an alias of `Shell` (routing, `is_shell_tool`, and `block_until_ms` normalization), but the codec only matched `shell`: - `tool_placeholder` returned `unsupported tool: bash`, which aborts the turn while streaming the tool call, before it ever runs; - `request` returned `tool bash is not executed through ExecServerMessage` (after already reserving an exec slot); and - `stream_closed` built its shell-specific error result only for `Shell`. Anthropic models frequently emit `Bash` even when the tool is advertised as `Shell`, so the alias must hold across the codec. Match `bash` wherever the codec special-cases `shell`. Co-Authored-By: Claude Opus 4.8 --- server/src/cursor/tools/codec/render.rs | 22 +++++++++++++- server/src/cursor/tools/codec/request.rs | 36 ++++++++++++++++++++++- server/src/cursor/tools/codec/response.rs | 3 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/server/src/cursor/tools/codec/render.rs b/server/src/cursor/tools/codec/render.rs index 7cc242f..d6c1fb3 100644 --- a/server/src/cursor/tools/codec/render.rs +++ b/server/src/cursor/tools/codec/render.rs @@ -167,7 +167,7 @@ pub fn tool_completed(call: &ToolCall, completion: &ToolCompletion) -> pb::Agent pub fn tool_placeholder(name: &str, call_id: &str) -> Result { use pb::tool_call::Tool; let tool = match normalized(name).as_str() { - "shell" => Tool::ShellToolCall(pb::ShellToolCall::default()), + "shell" | "bash" => Tool::ShellToolCall(pb::ShellToolCall::default()), "delete" => Tool::DeleteToolCall(pb::DeleteToolCall::default()), "glob" => Tool::GlobToolCall(pb::GlobToolCall::default()), "grep" => Tool::GrepToolCall(pb::GrepToolCall::default()), @@ -527,3 +527,23 @@ fn now_ms() -> u64 { .unwrap_or_default() .as_millis() as u64 } + +#[cfg(test)] +mod tests { + use super::tool_placeholder; + use crate::cursor::protocol::proto::agent::v1 as pb; + + #[test] + fn bash_renders_as_a_shell_placeholder() { + // The dispatcher treats `bash`/`Bash` as a Shell alias, so the streaming + // placeholder must too; otherwise a `Bash` tool call aborts the turn with + // `unsupported tool: bash` before it ever runs. + for name in ["shell", "Shell", "bash", "Bash"] { + let tool = tool_placeholder(name, "call-1").unwrap().tool; + assert!( + matches!(tool, Some(pb::tool_call::Tool::ShellToolCall(_))), + "{name} should render as a Shell tool" + ); + } + } +} diff --git a/server/src/cursor/tools/codec/request.rs b/server/src/cursor/tools/codec/request.rs index e16dfab..60ec9f0 100644 --- a/server/src/cursor/tools/codec/request.rs +++ b/server/src/cursor/tools/codec/request.rs @@ -35,7 +35,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result { + "shell" | "bash" => { let command = string("command")?; let (simple_commands, parsing_result) = shell_command_metadata(&command); Message::ShellStreamArgs(pb::ShellArgs { @@ -520,3 +520,37 @@ fn prost_value(value: &Value) -> prost_types::Value { }; ProstValue { kind: Some(kind) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::request; + use crate::cursor::protocol::proto::agent::v1 as pb; + use crate::cursor::tools::runtime::ExecContext; + use crate::model::ToolCall; + + #[test] + fn bash_is_encoded_as_a_shell_exec_request() { + // The dispatcher routes `bash`/`Bash` to the shell executor, so the + // request codec must encode it as a Shell stream instead of erroring + // with `tool bash is not executed through ExecServerMessage`. + let call = ToolCall { + index: 0, + call_id: "call-1".into(), + model_call_id: "model-1".into(), + name: "Bash".into(), + arguments_text: String::new(), + arguments: json!({ "command": "ls -la" }), + }; + let message = request(1, &call, &ExecContext::default()).unwrap(); + let Some(pb::agent_server_message::Message::ExecServerMessage(exec)) = message.message + else { + panic!("expected an ExecServerMessage"); + }; + let Some(pb::exec_server_message::Message::ShellStreamArgs(args)) = exec.message else { + panic!("expected ShellStreamArgs"); + }; + assert_eq!(args.command, "ls -la"); + } +} diff --git a/server/src/cursor/tools/codec/response.rs b/server/src/cursor/tools/codec/response.rs index 74e127a..0cf889a 100644 --- a/server/src/cursor/tools/codec/response.rs +++ b/server/src/cursor/tools/codec/response.rs @@ -144,7 +144,8 @@ pub async fn stream_closed(id: u32, pending: &CursorToolRuntime) -> Result