implemented shell_exec

This commit is contained in:
Luxferre
2026-03-21 17:11:17 +02:00
parent a2af89e61f
commit 6e2294cb48
6 changed files with 101 additions and 2 deletions
+29 -2
View File
@@ -1,6 +1,6 @@
# Sidekick # Sidekick
Sidekick is a versatile agentic framework written in Go 1.25. It provides a robust architecture for building AI assistants with hierarchical orchestration, native Model Context Protocol (MCP) integration, and a rich terminal user interface (TUI). Sidekick is a simple but versatile agentic framework written in Go 1.25. It provides a robust architecture for building AI assistants with hierarchical orchestration, native Model Context Protocol (MCP) integration, and a rich terminal user interface (TUI).
## Features ## Features
@@ -9,8 +9,35 @@ Sidekick is a versatile agentic framework written in Go 1.25. It provides a robu
* **Intelligent Buffering:** Automatically buffers large tool outputs to prevent context window exhaustion, returning file pointers for significant payloads. * **Intelligent Buffering:** Automatically buffers large tool outputs to prevent context window exhaustion, returning file pointers for significant payloads.
* **Rich Terminal UI:** A beautiful, responsive TUI built with Charmbracelet's BubbleTea and Lipgloss, featuring status bars, role-based formatting, and dynamic viewports. * **Rich Terminal UI:** A beautiful, responsive TUI built with Charmbracelet's BubbleTea and Lipgloss, featuring status bars, role-based formatting, and dynamic viewports.
* **Advanced Prompt Templating:** Define complex, multi-line system prompts using TOML. Support for Go's `text/template` engine allows you to compose modular prompts from reusable snippets. * **Advanced Prompt Templating:** Define complex, multi-line system prompts using TOML. Support for Go's `text/template` engine allows you to compose modular prompts from reusable snippets.
* **Internal Tools:** Built-in tools for file I/O (`read_file`, `write_file`, `grep_file`, `edit_file`) and an optional, secure `shell_exec` tool for running POSIX shell commands.
* **Configuration Driven:** Easily configure models, agents, tools, and MCP servers via a declarative `config.toml` file. * **Configuration Driven:** Easily configure models, agents, tools, and MCP servers via a declarative `config.toml` file.
## Internal Tools
Sidekick includes a set of internal tools that are always available to agents. These tools provide common file operations and system access.
### File Tools
- `read_file`: Read a file with optional line windowing.
- `write_file`: Overwrite a file with new content.
- `grep_file`: Regex search with surrounding context.
- `edit_file`: Replace a string or block of lines in a file.
### shell_exec Tool
The `shell_exec` tool allows an agent to run arbitrary POSIX shell commands and receive combined stdout/stderr output.
**Security Warning:** This tool is extremely powerful and potentially dangerous. It is **disabled by default** and must be explicitly enabled for each agent in `config.toml`.
To enable `shell_exec` for an agent, add `enable_shell_exec = true` to its configuration:
```toml
[agents.coordinator]
role = "COORDINATOR"
model_id = "default"
enable_shell_exec = true # Security: explicitly enabled
toolsets = ["filesystem"]
```
## Installation ## Installation
### Prerequisites ### Prerequisites
@@ -181,4 +208,4 @@ func main() {
## Credits ## Credits
Created by Luxferre in 2026, released into public domain with no warranties. Created by Luxferre in 2026, released into the public domain with no warranties.
+3
View File
@@ -24,6 +24,9 @@ func NewSidekick(config AgentConfig, model ModelConfig, extTools []ToolDefinitio
ToolRegistry: make(map[string]ToolDefinition), ToolRegistry: make(map[string]ToolDefinition),
} }
for name, tool := range DefaultInternalTools() { for name, tool := range DefaultInternalTools() {
if name == "shell_exec" && !s.Config.EnableShellExec {
continue
}
s.registerTool(name, tool) s.registerTool(name, tool)
} }
for _, tool := range extTools { for _, tool := range extTools {
+1
View File
@@ -56,6 +56,7 @@ type AgentConfig struct {
MaxIterations int `toml:"max_iterations"` MaxIterations int `toml:"max_iterations"`
Toolsets []string `toml:"toolsets"` // Allowed tool namespaces (MCP servers) Toolsets []string `toml:"toolsets"` // Allowed tool namespaces (MCP servers)
Subagents []string `toml:"subagents"` // Allowed subagent IDs Subagents []string `toml:"subagents"` // Allowed subagent IDs
EnableShellExec bool `toml:"enable_shell_exec"`
ToolResponseThreshold int `toml:"-"` ToolResponseThreshold int `toml:"-"`
} }
+35
View File
@@ -3,6 +3,7 @@ package sidekick
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
) )
@@ -69,9 +70,43 @@ func DefaultInternalTools() map[string]ToolDefinition {
Internal: true, Internal: true,
Handler: editFileHandler, Handler: editFileHandler,
}, },
"shell_exec": {
Name: "shell_exec",
Description: "Run a POSIX shell command and return its combined stdout and stderr.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"command": map[string]interface{}{"type": "string"},
},
"required": []string{"command"},
},
Internal: true,
Handler: shellExecHandler,
},
} }
} }
func shellExecHandler(args map[string]interface{}) (string, error) {
cmdStr, ok := args["command"].(string)
if !ok {
return "", fmt.Errorf("command is required")
}
cmd := exec.Command("sh", "-c", cmdStr)
output, err := cmd.CombinedOutput()
result := string(output)
if err != nil {
result = fmt.Sprintf("%s\nError: %v", result, err)
}
if result == "" {
return "(empty output)", nil
}
return result, nil
}
func readFileHandler(args map[string]interface{}) (string, error) { func readFileHandler(args map[string]interface{}) (string, error) {
path, ok := args["path"].(string) path, ok := args["path"].(string)
if !ok { if !ok {
+17
View File
@@ -6,6 +6,23 @@ import (
"testing" "testing"
) )
func TestShellExecHandler(t *testing.T) {
// Success
got, err := shellExecHandler(map[string]interface{}{"command": "echo hello"})
if err != nil { t.Fatal(err) }
if strings.TrimSpace(got) != "hello" { t.Errorf("expected hello, got %s", got) }
// Failure
got, err = shellExecHandler(map[string]interface{}{"command": "ls /nonexistent-file-path-that-should-not-exist"})
if err != nil { t.Fatal(err) }
if !strings.Contains(got, "Error:") { t.Errorf("expected error in output, got %s", got) }
// Empty
got, err = shellExecHandler(map[string]interface{}{"command": "true"})
if err != nil { t.Fatal(err) }
if got != "(empty output)" { t.Errorf("expected empty output, got %s", got) }
}
func TestHashLine(t *testing.T) { func TestHashLine(t *testing.T) {
line := "hello world" line := "hello world"
h1 := hashLine(line) h1 := hashLine(line)
+16
View File
@@ -79,6 +79,22 @@ func TestDelegation(t *testing.T) {
if ans != "Coord done" { t.Errorf("expected Coord done, got %s", ans) } if ans != "Coord done" { t.Errorf("expected Coord done, got %s", ans) }
} }
func TestShellExecRegistration(t *testing.T) {
// Scenario 1: Disabled (default)
c1 := AgentConfig{ID: "a1", EnableShellExec: false}
a1 := NewSidekick(c1, ModelConfig{}, nil)
if _, ok := a1.ToolRegistry["shell_exec"]; ok {
t.Errorf("shell_exec should be disabled by default")
}
// Scenario 2: Enabled
c2 := AgentConfig{ID: "a2", EnableShellExec: true}
a2 := NewSidekick(c2, ModelConfig{}, nil)
if _, ok := a2.ToolRegistry["shell_exec"]; !ok {
t.Errorf("shell_exec should be enabled when EnableShellExec is true")
}
}
func TestTempFileCleanup(t *testing.T) { func TestTempFileCleanup(t *testing.T) {
threshold := 10 threshold := 10
a := NewSidekick(AgentConfig{MaxIterations: 2, ToolResponseThreshold: threshold}, ModelConfig{}, nil) a := NewSidekick(AgentConfig{MaxIterations: 2, ToolResponseThreshold: threshold}, ModelConfig{}, nil)