pkg/tools/bash_tool.go (103 lines of code) (raw):

// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tools import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "github.com/GoogleCloudPlatform/kubectl-ai/gollm" ) func init() { RegisterTool(&BashTool{}) } const ( bashBin = "/bin/bash" ) // expandShellVar expands shell variables and syntax using bash func expandShellVar(value string) (string, error) { if strings.Contains(value, "~") { cmd := exec.Command(bashBin, "-c", fmt.Sprintf("echo %s", value)) output, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(output)), nil } return os.ExpandEnv(value), nil } type BashTool struct{} func (t *BashTool) Name() string { return "bash" } func (t *BashTool) Description() string { return "Executes a bash command. Use this tool only when you need to execute a shell command." } func (t *BashTool) FunctionDefinition() *gollm.FunctionDefinition { return &gollm.FunctionDefinition{ Name: t.Name(), Description: t.Description(), Parameters: &gollm.Schema{ Type: gollm.TypeObject, Properties: map[string]*gollm.Schema{ "command": { Type: gollm.TypeString, Description: `The bash command to execute.`, }, "modifies_resource": { Type: gollm.TypeString, Description: `Whether the command modifies a kubernetes resource. Possible values: - "yes" if the command modifies a resource - "no" if the command does not modify a resource - "unknown" if the command's effect on the resource is unknown `, }, }, }, } } func (t *BashTool) Run(ctx context.Context, args map[string]any) (any, error) { kubeconfig := ctx.Value("kubeconfig").(string) workDir := ctx.Value("work_dir").(string) command := args["command"].(string) if strings.Contains(command, "kubectl edit") { return &ExecResult{Error: "interactive mode not supported for kubectl, please use non-interactive commands"}, nil } if strings.Contains(command, "kubectl port-forward") { return &ExecResult{Error: "port-forwarding is not allowed because assistant is running in an unattended mode, please try some other alternative"}, nil } cmd := exec.CommandContext(ctx, bashBin, "-c", command) cmd.Dir = workDir cmd.Env = os.Environ() if kubeconfig != "" { kubeconfig, err := expandShellVar(kubeconfig) if err != nil { return nil, err } cmd.Env = append(cmd.Env, "KUBECONFIG="+kubeconfig) } return executeCommand(cmd) } type ExecResult struct { Error string `json:"error,omitempty"` Stdout string `json:"stdout,omitempty"` Stderr string `json:"stderr,omitempty"` ExitCode int `json:"exit_code,omitempty"` } func executeCommand(cmd *exec.Cmd) (*ExecResult, error) { var stdout bytes.Buffer cmd.Stdout = &stdout var stderr bytes.Buffer cmd.Stderr = &stderr results := &ExecResult{} if err := cmd.Run(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { results.ExitCode = exitError.ExitCode() } else { return nil, err } } results.Stdout = stdout.String() results.Stderr = stderr.String() return results, nil }