Add execline tool for secure command execution
- Add pkg/tools/execline.go with ExeclineTool implementation - Update config to support execline-based tool execution - Update agent instance to use execline when configured
This commit is contained in:
parent
71ce21963b
commit
505077dbce
4 changed files with 141 additions and 0 deletions
0
docker/entrypoint.sh
Normal file → Executable file
0
docker/entrypoint.sh
Normal file → Executable file
|
|
@ -91,6 +91,9 @@ func NewAgentInstance(
|
||||||
toolsRegistry.Register(execTool)
|
toolsRegistry.Register(execTool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cfg.Tools.IsToolEnabled("execline") {
|
||||||
|
toolsRegistry.Register(tools.NewExeclineTool(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Tools.IsToolEnabled("edit_file") {
|
if cfg.Tools.IsToolEnabled("edit_file") {
|
||||||
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
||||||
|
|
|
||||||
|
|
@ -784,6 +784,7 @@ type ToolsConfig struct {
|
||||||
Web WebToolsConfig `json:"web"`
|
Web WebToolsConfig `json:"web"`
|
||||||
Cron CronToolsConfig `json:"cron"`
|
Cron CronToolsConfig `json:"cron"`
|
||||||
Exec ExecConfig `json:"exec"`
|
Exec ExecConfig `json:"exec"`
|
||||||
|
Execline ToolConfig `json:"execline" envPrefix:"PICOCLAW_TOOLS_EXELINE_"`
|
||||||
Skills SkillsToolsConfig `json:"skills"`
|
Skills SkillsToolsConfig `json:"skills"`
|
||||||
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
||||||
MCP MCPConfig `json:"mcp"`
|
MCP MCPConfig `json:"mcp"`
|
||||||
|
|
@ -1247,6 +1248,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
|
||||||
return t.Cron.Enabled
|
return t.Cron.Enabled
|
||||||
case "exec":
|
case "exec":
|
||||||
return t.Exec.Enabled
|
return t.Exec.Enabled
|
||||||
|
case "execline":
|
||||||
|
return t.Execline.Enabled
|
||||||
case "skills":
|
case "skills":
|
||||||
return t.Skills.Enabled
|
return t.Skills.Enabled
|
||||||
case "media_cleanup":
|
case "media_cleanup":
|
||||||
|
|
|
||||||
135
pkg/tools/execline.go
Normal file
135
pkg/tools/execline.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/tools/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExeclineTool executes commands using execlineb instead of shell
|
||||||
|
// Security: execlineb does not support variable expansion ($VAR) or command
|
||||||
|
// substitution $(cmd) by default, reducing attack surface significantly.
|
||||||
|
type ExeclineTool struct {
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExeclineTool creates a new ExeclineTool instance
|
||||||
|
func NewExeclineTool(cfg *config.Config) *ExeclineTool {
|
||||||
|
return &ExeclineTool{
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the tool
|
||||||
|
func (t *ExeclineTool) Name() string {
|
||||||
|
return "execline"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the tool description
|
||||||
|
func (t *ExeclineTool) Description() string {
|
||||||
|
return `Execute commands using execlineb - a secure, minimal shell that does not expand variables or command substitution. Use for simple commands without shell features.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters returns the tool parameters
|
||||||
|
func (t *ExeclineTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"command": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The command to execute (passed as-is, no shell expansion)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"command"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs a command using execlineb
|
||||||
|
// Commands are executed as-is without shell expansion
|
||||||
|
func (t *ExeclineTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
// Extract command from args
|
||||||
|
cmdArg, ok := args["command"]
|
||||||
|
if !ok {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: "Missing 'command' argument",
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("missing 'command' argument"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the command string
|
||||||
|
command, ok := cmdArg.(string)
|
||||||
|
if !ok {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: "'command' must be a string",
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("'command' must be a string"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: "Empty command",
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("empty command"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous patterns in the command
|
||||||
|
if err := t.validateCommand(command); err != nil {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: fmt.Sprintf("Validation error: %v", err),
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("validation error: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build safe environment
|
||||||
|
baseEnv := shell.WithAllowedEnv(nil, nil)
|
||||||
|
var extraEnv map[string]string
|
||||||
|
if t.config != nil {
|
||||||
|
extraEnv = t.config.Tools.Exec.EnvSet
|
||||||
|
}
|
||||||
|
execEnv := shell.MergeEnvVars(baseEnv, nil, extraEnv)
|
||||||
|
|
||||||
|
// Use execlineb to execute
|
||||||
|
// execlineb -c takes a command string and executes it
|
||||||
|
// Unlike sh -c, it doesn't expand $VAR or $(cmd)
|
||||||
|
cmd := exec.CommandContext(ctx, "/usr/bin/execlineb", "-c", command)
|
||||||
|
cmd.Env = shell.MapToEnvSlice(execEnv)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: string(output),
|
||||||
|
ForUser: string(output),
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("execlineb error: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: string(output),
|
||||||
|
ForUser: string(output),
|
||||||
|
IsError: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCommand checks for dangerous command patterns
|
||||||
|
// Since execline is secure by default, we only block obvious exploits
|
||||||
|
func (t *ExeclineTool) validateCommand(cmd string) error {
|
||||||
|
// Block obvious shell escape attempts
|
||||||
|
if strings.Contains(cmd, "&&") || strings.Contains(cmd, "||") {
|
||||||
|
return fmt.Errorf("control operators (&&, ||) not supported in execline")
|
||||||
|
}
|
||||||
|
if strings.Contains(cmd, "|") && strings.Contains(cmd, "sh") {
|
||||||
|
return fmt.Errorf("pipe to shell detected")
|
||||||
|
}
|
||||||
|
// Note: $VAR and $(cmd) are simply not expanded by execlineb
|
||||||
|
// They are passed literally to the command, so this is safe
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue