Module: RubyLLM::Toolbox::Tools::GitHelpers

Included in:
ApplyPatch, GitAdd, GitBlame, GitBranch, GitCheckout, GitCommit, GitDiff, GitGrep, GitLog, GitShow, GitStatus
Defined in:
lib/ruby_llm/toolbox/tools/git_helpers.rb

Overview

Shared behavior for the git tools. Mixed into each so they all run git the same hardened way.

Hardening matters here because git executes commands configured by the repository in several places, which turns "read-only" commands into RCE when operating on an untrusted checkout:

- core.fsmonitor runs a command during `git status`
- diff.external / textconv run commands during `git diff`

The read tools are on by default, so the runner neutralizes those (-c core.fsmonitor=, and diff adds --no-ext-diff --no-textconv). The pager and credential prompts are also disabled so nothing can hang.

Constant Summary collapse

GIT_ENV =
{ "GIT_PAGER" => "cat", "GIT_TERMINAL_PROMPT" => "0" }.freeze
REF_RE =

A ref/branch the model supplies: no leading dash (option injection), and only the characters git refs legitimately use.

%r{\A[A-Za-z0-9][\w./\-]*\z}

Instance Method Summary collapse

Instance Method Details

#git_result(out, err, status) ⇒ Object

Maps a finished git run onto the toolbox return contract.



33
34
35
36
37
38
39
40
# File 'lib/ruby_llm/toolbox/tools/git_helpers.rb', line 33

def git_result(out, err, status)
  return error("git timed out after #{config.command_timeout}s", code: :timeout) if status == :timeout
  return out if status.exitstatus&.zero?

  message = (err.empty? ? out : err).strip
  code = message.match?(/not a git repository/i) ? :not_a_repo : :git_error
  error("git failed: #{message}", code: code)
end

#repo_relative(path) ⇒ Object

Resolve a path param to a repo-relative path, rejecting jail escapes. Returns nil for a blank path. Raises PathJail::Jailbreak on escape.



48
49
50
51
52
53
54
# File 'lib/ruby_llm/toolbox/tools/git_helpers.rb', line 48

def repo_relative(path)
  return nil if path.nil? || path.to_s.empty?

  jail = Safety::PathJail.new(config.fs_root)
  real = jail.resolve(path)
  Pathname.new(real).relative_path_from(Pathname.new(jail.root)).to_s
end

#run_git(*args, stdin: nil) ⇒ Object



27
28
29
30
# File 'lib/ruby_llm/toolbox/tools/git_helpers.rb', line 27

def run_git(*args, stdin: nil)
  argv = ["git", "-c", "core.fsmonitor=", "-C", config.fs_root, "--no-pager", *args]
  ProcessRunner.capture(argv, env: git_env, stdin: stdin, timeout: config.command_timeout, unsetenv_others: true)
end

#valid_ref?(ref) ⇒ Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/ruby_llm/toolbox/tools/git_helpers.rb', line 42

def valid_ref?(ref)
  ref.to_s.match?(REF_RE)
end