on main help sign in
fix(grep): do not go outside cwd, add timeout (#2188)
5b2a0bfCarlos Alexandro Becker
internal/agent/agentic_fetch_tool.go+1−1
@@ -167,7 +167,7 @@ func (c *coordinator) agenticFetchTool(_ context.Context, client *http.Client) (
167 webFetchTool,
167 webFetchTool,
168 webSearchTool,
168 webSearchTool,
169 tools.NewGlobTool(tmpDir),
169 tools.NewGlobTool(tmpDir),
170− tools.NewGrepTool(tmpDir),
170+ tools.NewGrepTool(tmpDir, c.cfg.Tools.Grep),
171 tools.NewSourcegraphTool(client),
171 tools.NewSourcegraphTool(client),
172 tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, tmpDir),
172 tools.NewViewTool(c.lspManager, c.permissions, c.filetracker, tmpDir),
173 }
173 }
internal/agent/common_test.go+1−1
@@ -208,7 +208,7 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel
208 tools.NewMultiEditTool(nil, env.permissions, env.history, *env.filetracker, env.workingDir),
208 tools.NewMultiEditTool(nil, env.permissions, env.history, *env.filetracker, env.workingDir),
209 tools.NewFetchTool(env.permissions, env.workingDir, r.GetDefaultClient()),
209 tools.NewFetchTool(env.permissions, env.workingDir, r.GetDefaultClient()),
210 tools.NewGlobTool(env.workingDir),
210 tools.NewGlobTool(env.workingDir),
211− tools.NewGrepTool(env.workingDir),
211+ tools.NewGrepTool(env.workingDir, cfg.Tools.Grep),
212 tools.NewLsTool(env.permissions, env.workingDir, cfg.Tools.Ls),
212 tools.NewLsTool(env.permissions, env.workingDir, cfg.Tools.Ls),
213 tools.NewSourcegraphTool(r.GetDefaultClient()),
213 tools.NewSourcegraphTool(r.GetDefaultClient()),
214 tools.NewViewTool(nil, env.permissions, *env.filetracker, env.workingDir),
214 tools.NewViewTool(nil, env.permissions, *env.filetracker, env.workingDir),
internal/agent/coordinator.go+1−1
@@ -426,7 +426,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
426 tools.NewMultiEditTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
426 tools.NewMultiEditTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
427 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
427 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
428 tools.NewGlobTool(c.cfg.WorkingDir()),
428 tools.NewGlobTool(c.cfg.WorkingDir()),
429− tools.NewGrepTool(c.cfg.WorkingDir()),
429+ tools.NewGrepTool(c.cfg.WorkingDir(), c.cfg.Tools.Grep),
430 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
430 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
431 tools.NewSourcegraphTool(nil),
431 tools.NewSourcegraphTool(nil),
432 tools.NewTodosTool(c.sessions),
432 tools.NewTodosTool(c.sessions),
internal/agent/tools/grep.go+6−3
@@ -18,6 +18,7 @@ import (
18 "time"
18 "time"
19
19
20 "charm.land/fantasy"
20 "charm.land/fantasy"
21+ "github.com/charmbracelet/crush/internal/config"
21 "github.com/charmbracelet/crush/internal/csync"
22 "github.com/charmbracelet/crush/internal/csync"
22 "github.com/charmbracelet/crush/internal/fsext"
23 "github.com/charmbracelet/crush/internal/fsext"
23)
24)
@@ -100,7 +101,7 @@ func escapeRegexPattern(pattern string) string {
100 return escaped
101 return escaped
101}
102}
102
103
103−func NewGrepTool(workingDir string) fantasy.AgentTool {
104+func NewGrepTool(workingDir string, config config.ToolGrep) fantasy.AgentTool {
104 return fantasy.NewAgentTool(
105 return fantasy.NewAgentTool(
105 GrepToolName,
106 GrepToolName,
106 string(grepDescription),
107 string(grepDescription),
@@ -109,7 +110,6 @@ func NewGrepTool(workingDir string) fantasy.AgentTool {
109 return fantasy.NewTextErrorResponse("pattern is required"), nil
110 return fantasy.NewTextErrorResponse("pattern is required"), nil
110 }
111 }
111
112
112− // If literal_text is true, escape the pattern
113 searchPattern := params.Pattern
113 searchPattern := params.Pattern
114 if params.LiteralText {
114 if params.LiteralText {
115 searchPattern = escapeRegexPattern(params.Pattern)
115 searchPattern = escapeRegexPattern(params.Pattern)
@@ -120,7 +120,10 @@ func NewGrepTool(workingDir string) fantasy.AgentTool {
120 searchPath = workingDir
120 searchPath = workingDir
121 }
121 }
122
122
123+ searchCtx, cancel := context.WithTimeout(ctx, config.GetTimeout())
124+ defer cancel()
125+
123− matches, truncated, err := searchFiles(ctx, searchPattern, searchPath, params.Include, 100)
126+ matches, truncated, err := searchFiles(searchCtx, searchPattern, searchPath, params.Include, 100)
124 if err != nil {
127 if err != nil {
125 return fantasy.NewTextErrorResponse(fmt.Sprintf("error searching files: %v", err)), nil
128 return fantasy.NewTextErrorResponse(fmt.Sprintf("error searching files: %v", err)), nil
126 }
129 }
internal/config/config.go+12−1
@@ -350,7 +350,8 @@ type Agent struct {
350}
350}
351
351
352type Tools struct {
352type Tools struct {
353− Ls ToolLs `json:"ls,omitzero"`
353+ Ls ToolLs `json:"ls,omitzero"`
354+ Grep ToolGrep `json:"grep,omitzero"`
354}
355}
355
356
356type ToolLs struct {
357type ToolLs struct {
@@ -358,10 +359,20 @@ type ToolLs struct {
358 MaxItems *int `json:"max_items,omitempty" jsonschema:"description=Maximum number of items to return for the ls tool,default=1000,example=100"`
359 MaxItems *int `json:"max_items,omitempty" jsonschema:"description=Maximum number of items to return for the ls tool,default=1000,example=100"`
359}
360}
360
361
362+// Limits returns the user-defined max-depth and max-items, or their defaults.
361func (t ToolLs) Limits() (depth, items int) {
363func (t ToolLs) Limits() (depth, items int) {
362 return ptrValOr(t.MaxDepth, 0), ptrValOr(t.MaxItems, 0)
364 return ptrValOr(t.MaxDepth, 0), ptrValOr(t.MaxItems, 0)
363}
365}
364
366
367+type ToolGrep struct {
368+ Timeout *time.Duration `json:"timeout,omitempty" jsonschema:"description=Timeout for the grep tool call,default=5s,example=10s"`
369+}
370+
371+// GetTimeout returns the user-defined timeout or the default.
372+func (t ToolGrep) GetTimeout() time.Duration {
373+ return ptrValOr(t.Timeout, 5*time.Second)
374+}
375+
365// Config holds the configuration for crush.
376// Config holds the configuration for crush.
366type Config struct {
377type Config struct {
367 Schema string `json:"$schema,omitempty"`
378 Schema string `json:"$schema,omitempty"`