implemented shell_exec
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user