Class: RubyLLM::Toolbox::Tools::GrepFiles
- Defined in:
- lib/ruby_llm/toolbox/tools/grep_files.rb
Overview
SAFE. Searches file contents for a regular expression within fs_root and returns “path:line: text” hits.
The pattern comes from the model, so two abuse vectors are guarded:
- ReDoS: the regex is compiled with a per-match timeout (Ruby 3.2+),
so catastrophic backtracking can't hang the process.
- Traversal: the walk starts at the jailed root, prunes noisy/VCS
directories, and skips symlinks so it can't be led outside.
Constant Summary collapse
- MAX_FILE_BYTES =
5 * 1024 * 1024
- MAX_CONTEXT =
50
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
Methods inherited from Base
#call, exec_tool!, exec_tool?, #initialize, #name
Constructor Details
This class inherits a constructor from RubyLLM::Toolbox::Base
Instance Method Details
#execute(pattern:, path: ".", glob: nil, ignore_case: false, context: nil, before: nil, after: nil) ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/ruby_llm/toolbox/tools/grep_files.rb', line 50 def execute(pattern:, path: ".", glob: nil, ignore_case: false, context: nil, before: nil, after: nil) regex = build_regex(pattern, ignore_case) jail = Safety::PathJail.new(config.fs_root) root = jail.resolve(path) return error("not a directory: #{path}", code: :not_a_directory) unless File.directory?(root) ctx_before = clamp_context(before.nil? ? context : before) ctx_after = clamp_context(after.nil? ? context : after) if ctx_before.zero? && ctx_after.zero? matches, capped = gather(root, regex, glob) return "no matches for #{pattern.inspect}" if matches.empty? body = +"#{matches.size}#{capped ? '+' : ''} match#{matches.size == 1 ? '' : 'es'} " \ "for #{pattern.inspect}:\n" body << matches.join("\n") truncate(body) else blocks, total, capped = gather_with_context(root, regex, glob, ctx_before, ctx_after) return "no matches for #{pattern.inspect}" if total.zero? body = +"#{total}#{capped ? '+' : ''} match#{total == 1 ? '' : 'es'} " \ "for #{pattern.inspect}:\n" body << blocks.join("\n--\n") truncate(body) end rescue Safety::PathJail::Jailbreak => e error(e., code: :path_denied) rescue Regexp::TimeoutError error("regex timed out (possible catastrophic backtracking); simplify the pattern", code: :regex_timeout) rescue RegexpError => e error("invalid regex: #{e.}", code: :bad_pattern) end |