Files
go-llm/v2/tools/exec.go
Steve Dudenhoeffer a4cb4baab5 Add go-llm v2: redesigned API for simpler LLM abstraction
v2 is a new Go module (v2/) with a dramatically simpler API:
- Unified Message type (no more Input marker interface)
- Define[T] for ergonomic tool creation with standard context.Context
- Chat session with automatic tool-call loop (agent loop)
- Streaming via pull-based StreamReader
- MCP one-call connect (MCPStdioServer, MCPHTTPServer, MCPSSEServer)
- Middleware support (logging, retry, timeout, usage tracking)
- Decoupled JSON Schema (map[string]any, no provider coupling)
- Sample tools: WebSearch, Browser, Exec, ReadFile, WriteFile, HTTP
- Providers: OpenAI, Anthropic, Google (all with streaming)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 20:00:08 -05:00

102 lines
2.3 KiB
Go

package tools
import (
"context"
"fmt"
"os/exec"
"runtime"
"strings"
"time"
llm "gitea.stevedudenhoeffer.com/steve/go-llm/v2"
)
// ExecParams defines parameters for the exec tool.
type ExecParams struct {
Command string `json:"command" description:"The shell command to execute"`
}
// ExecOption configures the exec tool.
type ExecOption func(*execConfig)
type execConfig struct {
allowedCommands []string
workDir string
timeout time.Duration
}
// WithAllowedCommands restricts which commands can be executed.
// If empty, all commands are allowed.
func WithAllowedCommands(cmds []string) ExecOption {
return func(c *execConfig) { c.allowedCommands = cmds }
}
// WithWorkDir sets the working directory for command execution.
func WithWorkDir(dir string) ExecOption {
return func(c *execConfig) { c.workDir = dir }
}
// WithExecTimeout sets the maximum execution time.
func WithExecTimeout(d time.Duration) ExecOption {
return func(c *execConfig) { c.timeout = d }
}
// Exec creates a shell command execution tool.
//
// Example:
//
// tools := llm.NewToolBox(
// tools.Exec(tools.WithAllowedCommands([]string{"ls", "cat", "grep"})),
// )
func Exec(opts ...ExecOption) llm.Tool {
cfg := &execConfig{
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(cfg)
}
return llm.Define[ExecParams]("exec", "Execute a shell command and return its output",
func(ctx context.Context, p ExecParams) (string, error) {
// Check allowed commands
if len(cfg.allowedCommands) > 0 {
parts := strings.Fields(p.Command)
if len(parts) == 0 {
return "", fmt.Errorf("empty command")
}
allowed := false
for _, cmd := range cfg.allowedCommands {
if parts[0] == cmd {
allowed = true
break
}
}
if !allowed {
return "", fmt.Errorf("command %q is not in the allowed list", parts[0])
}
}
ctx, cancel := context.WithTimeout(ctx, cfg.timeout)
defer cancel()
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.CommandContext(ctx, "cmd", "/C", p.Command)
} else {
cmd = exec.CommandContext(ctx, "sh", "-c", p.Command)
}
if cfg.workDir != "" {
cmd.Dir = cfg.workDir
}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Sprintf("Error: %s\nOutput: %s", err.Error(), string(output)), nil
}
return string(output), nil
},
)
}