implemented shell_exec
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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.
|
||||
* **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.
|
||||
* **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.
|
||||
|
||||
## 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
|
||||
|
||||
### Prerequisites
|
||||
@@ -181,4 +208,4 @@ func main() {
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -24,6 +24,9 @@ func NewSidekick(config AgentConfig, model ModelConfig, extTools []ToolDefinitio
|
||||
ToolRegistry: make(map[string]ToolDefinition),
|
||||
}
|
||||
for name, tool := range DefaultInternalTools() {
|
||||
if name == "shell_exec" && !s.Config.EnableShellExec {
|
||||
continue
|
||||
}
|
||||
s.registerTool(name, tool)
|
||||
}
|
||||
for _, tool := range extTools {
|
||||
|
||||
@@ -56,6 +56,7 @@ type AgentConfig struct {
|
||||
MaxIterations int `toml:"max_iterations"`
|
||||
Toolsets []string `toml:"toolsets"` // Allowed tool namespaces (MCP servers)
|
||||
Subagents []string `toml:"subagents"` // Allowed subagent IDs
|
||||
EnableShellExec bool `toml:"enable_shell_exec"`
|
||||
ToolResponseThreshold int `toml:"-"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package sidekick
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@@ -69,9 +70,43 @@ func DefaultInternalTools() map[string]ToolDefinition {
|
||||
Internal: true,
|
||||
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) {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
|
||||
@@ -6,6 +6,23 @@ import (
|
||||
"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) {
|
||||
line := "hello world"
|
||||
h1 := hashLine(line)
|
||||
|
||||
@@ -79,6 +79,22 @@ func TestDelegation(t *testing.T) {
|
||||
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) {
|
||||
threshold := 10
|
||||
a := NewSidekick(AgentConfig{MaxIterations: 2, ToolResponseThreshold: threshold}, ModelConfig{}, nil)
|
||||
|
||||
Reference in New Issue
Block a user