From 578793be1dc3f37b7971366bfbf9fec932a6116f Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 5 Jun 2026 19:02:52 +0300 Subject: [PATCH] fix(coderd/x/chatd): disclose execute shell is POSIX sh (#26101) --- coderd/x/chatd/chattool/execute.go | 4 ++-- coderd/x/chatd/chattool/execute_test.go | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/coderd/x/chatd/chattool/execute.go b/coderd/x/chatd/chattool/execute.go index a56d0cc29d..0b483dc386 100644 --- a/coderd/x/chatd/chattool/execute.go +++ b/coderd/x/chatd/chattool/execute.go @@ -77,7 +77,7 @@ type ProcessToolOptions struct { // ExecuteArgs are the parameters accepted by the execute tool. type ExecuteArgs struct { - Command string `json:"command" description:"The shell command to execute."` + Command string `json:"command" description:"The shell command to execute. Runs under \"sh -c\" (POSIX)."` ModelIntent *string `json:"model_intent,omitempty" description:"A short, natural-language, present-participle phrase describing what you are doing. This is shown to the user alongside the command. Use plain English with no underscores or technical jargon. The UI appends \"using \" and \"for \" automatically, so do not repeat the command or include a duration. Keep it under 100 characters. Good examples: \"Running the unit tests\", \"Checking repository state\", \"Inspecting build output\"."` Timeout *string `json:"timeout,omitempty" description:"How long to wait for completion (e.g. '30s', '5m'). Default is 10s. The process keeps running if this expires and you get a background_process_id to re-attach. Only applies to foreground commands."` WorkDir *string `json:"workdir,omitempty" description:"Working directory for the command."` @@ -92,7 +92,7 @@ const ExecuteToolName = "execute" func Execute(options ExecuteOptions) fantasy.AgentTool { return fantasy.NewAgentTool( ExecuteToolName, - "Execute a shell command in the workspace. Runs the command and waits for completion up to the timeout (default 10s, override with the timeout parameter e.g. '30s', '5m'). If the command exceeds the timeout, the response includes a background_process_id; use process_output with that ID to re-attach and wait for the result. Use run_in_background=true for persistent processes (dev servers, file watchers) or when you want to continue other work while the command runs. Never use shell '&' for backgrounding.", + "Execute a shell command in the workspace. Runs under \"sh -c\" (POSIX). Waits for completion up to the timeout (default 10s, override with the timeout parameter e.g. '30s', '5m'). If the command exceeds the timeout, the response includes a background_process_id; use process_output with that ID to re-attach and wait for the result. Use run_in_background=true for persistent processes (dev servers, file watchers) or when you want to continue other work while the command runs. Never use shell '&' for backgrounding.", func(ctx context.Context, args ExecuteArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { if options.GetWorkspaceConn == nil { return fantasy.NewTextErrorResponse("workspace connection resolver is not configured"), nil diff --git a/coderd/x/chatd/chattool/execute_test.go b/coderd/x/chatd/chattool/execute_test.go index fefbd90a12..0ff98dd1e6 100644 --- a/coderd/x/chatd/chattool/execute_test.go +++ b/coderd/x/chatd/chattool/execute_test.go @@ -34,6 +34,19 @@ func TestExecuteTool(t *testing.T) { assert.NotContains(t, info.Required, "model_intent") }) + t.Run("SchemaDisclosesShell", func(t *testing.T) { + t.Parallel() + + tool := chattool.Execute(chattool.ExecuteOptions{}) + info := tool.Info() + assert.Contains(t, info.Description, `Runs under "sh -c" (POSIX)`) + + commandParam, ok := info.Parameters["command"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "string", commandParam["type"]) + assert.Contains(t, commandParam["description"], `Runs under "sh -c" (POSIX)`) + }) + t.Run("EmptyCommand", func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t)