Repository: GoogleCloudPlatform/kubectl-ai
File: pkg/tools/bash_tool.go (Go)

Code before:
	command := strings.Join(cmd.Args, " ")
	isWatch := strings.Contains(command, "kubectl get") && strings.Contains(command, "-w")
	isLogs := strings.Contains(command, "kubectl logs") && strings.Contains(command, "-f")
	isExec := strings.Contains(command, "kubectl exec") && strings.Contains(command, "-it")
	isAttach := strings.Contains(command, "kubectl attach")
	isPortForward := strings.Contains(command, "kubectl port-forward")

	// Block interactive commands
	if isExec || isPortForward {
		return &ExecResult{Error: "interactive mode not supported for kubectl, please use non-interactive commands"}, nil
	}

	// Handle streaming commands
	if isWatch || isLogs || isAttach {
		// Create a context with timeout
		ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
		defer cancel()

		// Create pipes for stdout and stderr
		stdout, err := cmd.StdoutPipe()
		if err != nil {
			return nil, fmt.Errorf("creating stdout pipe: %w", err)
		}
		stderr, err := cmd.StderrPipe()
		if err != nil {
			return nil, fmt.Errorf("creating stderr pipe: %w", err)
		}

		if err := cmd.Start(); err != nil {
			return nil, fmt.Errorf("starting command: %w", err)
		}

		// Read output in goroutines
		var stdoutBuilder, stderrBuilder strings.Builder
		stdoutDone := make(chan struct{})
		stderrDone := make(chan struct{})

		go func() {
			scanner := bufio.NewScanner(stdout)
			for scanner.Scan() {
				fmt.Println(scanner.Text())
				stdoutBuilder.WriteString(scanner.Text() + "\n")
			}
			close(stdoutDone)
		}()

		go func() {
			scanner := bufio.NewScanner(stderr)
			for scanner.Scan() {
				fmt.Fprintln(os.Stderr, scanner.Text())
				stderrBuilder.WriteString(scanner.Text() + "\n")

Review comment (security):
I have noticed that we will ask for user's permission to run a command and then later apply this filtering. Which is confusing and annoying. So I would suggest:
If we can refactor this filtering in a separate function, then we can call that function even before we ask for approval from users.

Also: Its okay to just introduce the filtering in a common function here, but actual change to avoid asking for approval for interactive/blocking command in a separate follow up PR.

Code after:
	// Inline isKubectlCommand logic
	words := strings.Fields(command)
	if len(words) == 0 {
		return false, nil
	}
	base := filepath.Base(words[0])
	if base != "kubectl" {
		return false, nil
	}

	isExec := strings.Contains(command, " exec ") && strings.Contains(command, " -it")
	isPortForward := strings.Contains(command, " port-forward ")
	isEdit := strings.Contains(command, " edit ")

	if isExec || isPortForward || isEdit {
		return true, fmt.Errorf("interactive mode not supported for kubectl, please use non-interactive commands")
	}
	return false, nil
}

func executeCommand(cmd *exec.Cmd) (*ExecResult, error) {
	command := strings.Join(cmd.Args, " ")

	if isInteractive, err := IsInteractiveCommand(command); isInteractive {
		return &ExecResult{Error: err.Error()}, nil
	}

	isWatch := strings.Contains(command, " get ") && strings.Contains(command, " -w")
	isLogs := strings.Contains(command, " logs ") && strings.Contains(command, " -f")
	isAttach := strings.Contains(command, " attach ")

	// Handle streaming commands
	if isWatch || isLogs || isAttach {
		// Create a context with timeout
		ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
		defer cancel()

		// Create pipes for stdout and stderr
		stdout, err := cmd.StdoutPipe()
		if err != nil {
			return nil, fmt.Errorf("creating stdout pipe: %w", err)
		}
		stderr, err := cmd.StderrPipe()
		if err != nil {
			return nil, fmt.Errorf("creating stderr pipe: %w", err)
		}

		if err := cmd.Start(); err != nil {
			return nil, fmt.Errorf("starting command: %w", err)
		}