From 5b2a0bf38e8dd67707e3d433e59de5740aedfc97 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 11 Feb 2026 16:10:36 -0300 Subject: [PATCH] fix(grep): do not go outside cwd, add timeout (#2188) * fix(grep): do not go outside cwd, add timeout Signed-off-by: Carlos Alexandro Becker * fix: timeout config Signed-off-by: Carlos Alexandro Becker --------- Signed-off-by: Carlos Alexandro Becker --- internal/agent/agentic_fetch_tool.go | 2 +- internal/agent/common_test.go | 2 +- internal/agent/coordinator.go | 2 +- internal/agent/tools/grep.go | 9 ++++++--- internal/config/config.go | 13 ++++++++++++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/agent/agentic_fetch_tool.go b/internal/agent/agentic_fetch_tool.go index 9bb27327516da66323fee454b9048cf5f9f69b6b..2d52814d446581fca0e7a98368ffaae465aedf2c 100644 --- a/internal/agent/agentic_fetch_tool.go +++ b/internal/agent/agentic_fetch_tool.go @@ -167,7 +167,7 @@ func (c *coordinator) agenticFetchTool(_ context.Context, client *http.Client) ( webFetchTool, webSearchTool, tools.NewGlobTool(tmpDir), - tools.NewGrepTool(tmpDir), + tools.NewGrepTool(tmpDir, c.cfg.Tools.Grep), tools.NewSourcegraphTool(client), tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, tmpDir), } diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go index 1a420e2b40b84027db7469a71ca9212b69f6e380..3ab3e68ec046dfb8db9dd0801c4a744c7e148bd2 100644 --- a/internal/agent/common_test.go +++ b/internal/agent/common_test.go @@ -208,7 +208,7 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel tools.NewMultiEditTool(nil, env.permissions, env.history, *env.filetracker, env.workingDir), tools.NewFetchTool(env.permissions, env.workingDir, r.GetDefaultClient()), tools.NewGlobTool(env.workingDir), - tools.NewGrepTool(env.workingDir), + tools.NewGrepTool(env.workingDir, cfg.Tools.Grep), tools.NewLsTool(env.permissions, env.workingDir, cfg.Tools.Ls), tools.NewSourcegraphTool(r.GetDefaultClient()), tools.NewViewTool(nil, env.permissions, *env.filetracker, env.workingDir), diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index a6048a7620bef5236ef8266612538685dcf48aac..a1fd6ae3dd293cac5ad65079c96bad4acd47c4f9 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -426,7 +426,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan tools.NewMultiEditTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()), tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil), tools.NewGlobTool(c.cfg.WorkingDir()), - tools.NewGrepTool(c.cfg.WorkingDir()), + tools.NewGrepTool(c.cfg.WorkingDir(), c.cfg.Tools.Grep), tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls), tools.NewSourcegraphTool(nil), tools.NewTodosTool(c.sessions), diff --git a/internal/agent/tools/grep.go b/internal/agent/tools/grep.go index 5059396ace28828e61d7beab2705f700d5f8b50f..8c7ec51e4e3768e4814b2d6baa3c7085357f5b45 100644 --- a/internal/agent/tools/grep.go +++ b/internal/agent/tools/grep.go @@ -18,6 +18,7 @@ import ( "time" "charm.land/fantasy" + "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/fsext" ) @@ -100,7 +101,7 @@ func escapeRegexPattern(pattern string) string { return escaped } -func NewGrepTool(workingDir string) fantasy.AgentTool { +func NewGrepTool(workingDir string, config config.ToolGrep) fantasy.AgentTool { return fantasy.NewAgentTool( GrepToolName, string(grepDescription), @@ -109,7 +110,6 @@ func NewGrepTool(workingDir string) fantasy.AgentTool { return fantasy.NewTextErrorResponse("pattern is required"), nil } - // If literal_text is true, escape the pattern searchPattern := params.Pattern if params.LiteralText { searchPattern = escapeRegexPattern(params.Pattern) @@ -120,7 +120,10 @@ func NewGrepTool(workingDir string) fantasy.AgentTool { searchPath = workingDir } - matches, truncated, err := searchFiles(ctx, searchPattern, searchPath, params.Include, 100) + searchCtx, cancel := context.WithTimeout(ctx, config.GetTimeout()) + defer cancel() + + matches, truncated, err := searchFiles(searchCtx, searchPattern, searchPath, params.Include, 100) if err != nil { return fantasy.NewTextErrorResponse(fmt.Sprintf("error searching files: %v", err)), nil } diff --git a/internal/config/config.go b/internal/config/config.go index 079c5fb4fbf7bd9814ac83a360af98c7dc404397..753151509315545dfbed9bd74c1455785313c8aa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -350,7 +350,8 @@ type Agent struct { } type Tools struct { - Ls ToolLs `json:"ls,omitzero"` + Ls ToolLs `json:"ls,omitzero"` + Grep ToolGrep `json:"grep,omitzero"` } type ToolLs struct { @@ -358,10 +359,20 @@ type ToolLs struct { MaxItems *int `json:"max_items,omitempty" jsonschema:"description=Maximum number of items to return for the ls tool,default=1000,example=100"` } +// Limits returns the user-defined max-depth and max-items, or their defaults. func (t ToolLs) Limits() (depth, items int) { return ptrValOr(t.MaxDepth, 0), ptrValOr(t.MaxItems, 0) } +type ToolGrep struct { + Timeout *time.Duration `json:"timeout,omitempty" jsonschema:"description=Timeout for the grep tool call,default=5s,example=10s"` +} + +// GetTimeout returns the user-defined timeout or the default. +func (t ToolGrep) GetTimeout() time.Duration { + return ptrValOr(t.Timeout, 5*time.Second) +} + // Config holds the configuration for crush. type Config struct { Schema string `json:"$schema,omitempty"`