fix(exec) fail close on invalid deny pattern (#781)

* fix(exec) fail close on invalid deny pattern

* fix: error check

* resolve conflicts
This commit is contained in:
Mauro 2026-02-28 09:24:26 +01:00 committed by GitHub
parent 6c8866de6f
commit 172e6ebe5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 77 additions and 22 deletions

View file

@ -3,6 +3,7 @@ package gateway
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@ -223,7 +224,11 @@ func setupCronTool(
cronService := cron.NewCronService(cronStorePath, nil) cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool // Create and register CronTool
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg) cronTool, err := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
if err != nil {
log.Fatalf("Critical error during CronTool initialization: %v", err)
}
agentLoop.RegisterTool(cronTool) agentLoop.RegisterTool(cronTool)
// Set the onJob handler // Set the onJob handler

View file

@ -1,6 +1,7 @@
package agent package agent
import ( import (
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -51,7 +52,12 @@ func NewAgentInstance(
toolsRegistry.Register(tools.NewReadFileTool(workspace, restrict)) toolsRegistry.Register(tools.NewReadFileTool(workspace, restrict))
toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict)) toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict))
toolsRegistry.Register(tools.NewListDirTool(workspace, restrict)) toolsRegistry.Register(tools.NewListDirTool(workspace, restrict))
toolsRegistry.Register(tools.NewExecToolWithConfig(workspace, restrict, cfg)) execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg)
if err != nil {
log.Fatalf("Critical error: unable to initialize exec tool: %v", err)
}
toolsRegistry.Register(execTool)
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict)) toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict))
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict)) toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict))

View file

@ -33,15 +33,19 @@ type CronTool struct {
func NewCronTool( func NewCronTool(
cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool,
execTimeout time.Duration, config *config.Config, execTimeout time.Duration, config *config.Config,
) *CronTool { ) (*CronTool, error) {
execTool := NewExecToolWithConfig(workspace, restrict, config) execTool, err := NewExecToolWithConfig(workspace, restrict, config)
if err != nil {
return nil, fmt.Errorf("unable to configure exec tool: %w", err)
}
execTool.SetTimeout(execTimeout) execTool.SetTimeout(execTimeout)
return &CronTool{ return &CronTool{
cronService: cronService, cronService: cronService,
executor: executor, executor: executor,
msgBus: msgBus, msgBus: msgBus,
execTool: execTool, execTool: execTool,
} }, nil
} }
// Name returns the tool name // Name returns the tool name

View file

@ -69,11 +69,11 @@ var defaultDenyPatterns = []*regexp.Regexp{
regexp.MustCompile(`\bsource\s+.*\.sh\b`), regexp.MustCompile(`\bsource\s+.*\.sh\b`),
} }
func NewExecTool(workingDir string, restrict bool) *ExecTool { func NewExecTool(workingDir string, restrict bool) (*ExecTool, error) {
return NewExecToolWithConfig(workingDir, restrict, nil) return NewExecToolWithConfig(workingDir, restrict, nil)
} }
func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) *ExecTool { func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) (*ExecTool, error) {
denyPatterns := make([]*regexp.Regexp, 0) denyPatterns := make([]*regexp.Regexp, 0)
if config != nil { if config != nil {
@ -86,8 +86,7 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf
for _, pattern := range execConfig.CustomDenyPatterns { for _, pattern := range execConfig.CustomDenyPatterns {
re, err := regexp.Compile(pattern) re, err := regexp.Compile(pattern)
if err != nil { if err != nil {
fmt.Printf("Invalid custom deny pattern %q: %v\n", pattern, err) return nil, fmt.Errorf("invalid custom deny pattern %q: %w", pattern, err)
continue
} }
denyPatterns = append(denyPatterns, re) denyPatterns = append(denyPatterns, re)
} }
@ -106,7 +105,7 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf
denyPatterns: denyPatterns, denyPatterns: denyPatterns,
allowPatterns: nil, allowPatterns: nil,
restrictToWorkspace: restrict, restrictToWorkspace: restrict,
} }, nil
} }
func (t *ExecTool) Name() string { func (t *ExecTool) Name() string {

View file

@ -11,7 +11,10 @@ import (
// TestShellTool_Success verifies successful command execution // TestShellTool_Success verifies successful command execution
func TestShellTool_Success(t *testing.T) { func TestShellTool_Success(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
args := map[string]any{ args := map[string]any{
@ -38,7 +41,10 @@ func TestShellTool_Success(t *testing.T) {
// TestShellTool_Failure verifies failed command execution // TestShellTool_Failure verifies failed command execution
func TestShellTool_Failure(t *testing.T) { func TestShellTool_Failure(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
args := map[string]any{ args := map[string]any{
@ -65,7 +71,11 @@ func TestShellTool_Failure(t *testing.T) {
// TestShellTool_Timeout verifies command timeout handling // TestShellTool_Timeout verifies command timeout handling
func TestShellTool_Timeout(t *testing.T) { func TestShellTool_Timeout(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
tool.SetTimeout(100 * time.Millisecond) tool.SetTimeout(100 * time.Millisecond)
ctx := context.Background() ctx := context.Background()
@ -93,7 +103,10 @@ func TestShellTool_WorkingDir(t *testing.T) {
testFile := filepath.Join(tmpDir, "test.txt") testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("test content"), 0o644) os.WriteFile(testFile, []byte("test content"), 0o644)
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
args := map[string]any{ args := map[string]any{
@ -114,7 +127,10 @@ func TestShellTool_WorkingDir(t *testing.T) {
// TestShellTool_DangerousCommand verifies safety guard blocks dangerous commands // TestShellTool_DangerousCommand verifies safety guard blocks dangerous commands
func TestShellTool_DangerousCommand(t *testing.T) { func TestShellTool_DangerousCommand(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
args := map[string]any{ args := map[string]any{
@ -135,7 +151,10 @@ func TestShellTool_DangerousCommand(t *testing.T) {
// TestShellTool_MissingCommand verifies error handling for missing command // TestShellTool_MissingCommand verifies error handling for missing command
func TestShellTool_MissingCommand(t *testing.T) { func TestShellTool_MissingCommand(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
args := map[string]any{} args := map[string]any{}
@ -150,7 +169,10 @@ func TestShellTool_MissingCommand(t *testing.T) {
// TestShellTool_StderrCapture verifies stderr is captured and included // TestShellTool_StderrCapture verifies stderr is captured and included
func TestShellTool_StderrCapture(t *testing.T) { func TestShellTool_StderrCapture(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
args := map[string]any{ args := map[string]any{
@ -170,7 +192,10 @@ func TestShellTool_StderrCapture(t *testing.T) {
// TestShellTool_OutputTruncation verifies long output is truncated // TestShellTool_OutputTruncation verifies long output is truncated
func TestShellTool_OutputTruncation(t *testing.T) { func TestShellTool_OutputTruncation(t *testing.T) {
tool := NewExecTool("", false) tool, err := NewExecTool("", false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
ctx := context.Background() ctx := context.Background()
// Generate long output (>10000 chars) // Generate long output (>10000 chars)
@ -198,7 +223,11 @@ func TestShellTool_WorkingDir_OutsideWorkspace(t *testing.T) {
t.Fatalf("failed to create outside dir: %v", err) t.Fatalf("failed to create outside dir: %v", err)
} }
tool := NewExecTool(workspace, true) tool, err := NewExecTool(workspace, true)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
result := tool.Execute(context.Background(), map[string]any{ result := tool.Execute(context.Background(), map[string]any{
"command": "pwd", "command": "pwd",
"working_dir": outsideDir, "working_dir": outsideDir,
@ -232,7 +261,11 @@ func TestShellTool_WorkingDir_SymlinkEscape(t *testing.T) {
t.Skipf("symlinks not supported in this environment: %v", err) t.Skipf("symlinks not supported in this environment: %v", err)
} }
tool := NewExecTool(workspace, true) tool, err := NewExecTool(workspace, true)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
result := tool.Execute(context.Background(), map[string]any{ result := tool.Execute(context.Background(), map[string]any{
"command": "cat secret.txt", "command": "cat secret.txt",
"working_dir": link, "working_dir": link,
@ -249,7 +282,11 @@ func TestShellTool_WorkingDir_SymlinkEscape(t *testing.T) {
// TestShellTool_RestrictToWorkspace verifies workspace restriction // TestShellTool_RestrictToWorkspace verifies workspace restriction
func TestShellTool_RestrictToWorkspace(t *testing.T) { func TestShellTool_RestrictToWorkspace(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
tool := NewExecTool(tmpDir, false) tool, err := NewExecTool(tmpDir, false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
tool.SetRestrictToWorkspace(true) tool.SetRestrictToWorkspace(true)
ctx := context.Background() ctx := context.Background()

View file

@ -22,7 +22,11 @@ func processExists(pid int) bool {
} }
func TestShellTool_TimeoutKillsChildProcess(t *testing.T) { func TestShellTool_TimeoutKillsChildProcess(t *testing.T) {
tool := NewExecTool(t.TempDir(), false) tool, err := NewExecTool(t.TempDir(), false)
if err != nil {
t.Errorf("unable to configure exec tool: %s", err)
}
tool.SetTimeout(500 * time.Millisecond) tool.SetTimeout(500 * time.Millisecond)
args := map[string]any{ args := map[string]any{