- Project infrastructure: go.mod, Makefile, .gitignore, README - Configuration system with YAML parsing and env overrides - Structured logging with slog - AI provider interface with Ollama, OpenAI, Anthropic implementations - Provider manager with runtime switching - Agent orchestrator with ReAct loop (reason-act-observe) - Tool registry with JSON Schema descriptions - Terminal tool: shell command execution with safety controls - Filesystem tools: read, write, list with path access control - Conversation memory with session management - Web UI server with WebSocket streaming - Modern dark theme UI with glassmorphism, animations - Frontend: WebSocket client, markdown rendering, tool call display
121 lines
2.9 KiB
Go
121 lines
2.9 KiB
Go
package terminal
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/ai"
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/config"
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/tools"
|
|
)
|
|
|
|
type runCommandArgs struct {
|
|
Command string `json:"command"`
|
|
Cwd string `json:"cwd,omitempty"`
|
|
}
|
|
|
|
// RegisterTools registers terminal-related tools in the registry.
|
|
func RegisterTools(registry *tools.Registry, cfg config.TerminalToolConfig, log *slog.Logger) {
|
|
if !cfg.Enabled {
|
|
return
|
|
}
|
|
|
|
registry.Register(tools.ToolDef{
|
|
Tool: ai.Tool{
|
|
Name: "run_command",
|
|
Description: "Execute a shell command in the terminal and return its output. Use this to run any command-line operations.",
|
|
Parameters: map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"command": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "The shell command to execute",
|
|
},
|
|
"cwd": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Working directory for the command (optional)",
|
|
},
|
|
},
|
|
"required": []string{"command"},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args json.RawMessage) (string, error) {
|
|
var a runCommandArgs
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return "", fmt.Errorf("parsing args: %w", err)
|
|
}
|
|
return runCommand(ctx, a, cfg, log)
|
|
},
|
|
})
|
|
}
|
|
|
|
func runCommand(ctx context.Context, args runCommandArgs, cfg config.TerminalToolConfig, log *slog.Logger) (string, error) {
|
|
// Check for blocked commands
|
|
cmdLower := strings.ToLower(args.Command)
|
|
for _, blocked := range cfg.BlockedCommands {
|
|
if strings.Contains(cmdLower, strings.ToLower(blocked)) {
|
|
return "", fmt.Errorf("command blocked by security policy: %s", blocked)
|
|
}
|
|
}
|
|
|
|
timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
shell := cfg.Shell
|
|
if shell == "" {
|
|
shell = "/bin/bash"
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, shell, "-c", args.Command)
|
|
if args.Cwd != "" {
|
|
cmd.Dir = args.Cwd
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
log.Info("running command", "command", args.Command, "cwd", args.Cwd)
|
|
|
|
err := cmd.Run()
|
|
|
|
var result strings.Builder
|
|
if stdout.Len() > 0 {
|
|
result.WriteString("STDOUT:\n")
|
|
result.WriteString(stdout.String())
|
|
}
|
|
if stderr.Len() > 0 {
|
|
if result.Len() > 0 {
|
|
result.WriteString("\n")
|
|
}
|
|
result.WriteString("STDERR:\n")
|
|
result.WriteString(stderr.String())
|
|
}
|
|
|
|
if err != nil {
|
|
if result.Len() > 0 {
|
|
result.WriteString("\n")
|
|
}
|
|
result.WriteString(fmt.Sprintf("EXIT ERROR: %v", err))
|
|
}
|
|
|
|
if result.Len() == 0 {
|
|
result.WriteString("(no output)")
|
|
}
|
|
|
|
// Truncate very long output
|
|
output := result.String()
|
|
const maxLen = 10000
|
|
if len(output) > maxLen {
|
|
output = output[:maxLen] + "\n... (output truncated)"
|
|
}
|
|
|
|
return output, nil
|
|
}
|