ZovOS_AI/internal/tools/terminal/terminal.go
Dan4ick 460d20539f feat: initial project structure - Phase 1 MVP
- 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
2026-03-20 00:38:22 +03:00

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
}