Class: Legion::CLI::Chat::Tools::RunCommand

Inherits:
Tools::Base
  • Object
show all
Defined in:
lib/legion/cli/chat/tools/run_command.rb

Class Method Summary collapse

Methods inherited from Tools::Base

deferred, deferred?, description, error_response, extension, handle_exception, input_schema, log, mcp_category, mcp_tier, runner, sticky, tags, text_response, tool_name, trigger_words

Class Method Details

.call(command:, timeout: 120, working_directory: nil) ⇒ Object



24
25
26
27
28
29
30
31
32
# File 'lib/legion/cli/chat/tools/run_command.rb', line 24

def self.call(command:, timeout: 120, working_directory: nil)
  dir = working_directory ? File.expand_path(working_directory) : Dir.pwd

  if sandbox_enabled? && sandbox_available?
    execute_sandboxed(command: command, timeout: timeout, dir: dir)
  else
    execute_direct(command: command, timeout: timeout, dir: dir)
  end
end

.execute_direct(command:, timeout:, dir:) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/legion/cli/chat/tools/run_command.rb', line 63

def self.execute_direct(command:, timeout:, dir:)
  stdout, stderr, status = Open3.popen3(command, chdir: dir) do |stdin, out, err, wait_thr|
    stdin.close
    out_reader = Thread.new { out.read }
    err_reader = Thread.new { err.read }

    unless wait_thr.join(timeout)
      ::Process.kill('TERM', wait_thr.pid)
      wait_thr.join(5) || ::Process.kill('KILL', wait_thr.pid)
      out_reader.kill
      err_reader.kill
      raise ::Timeout::Error, "command timed out after #{timeout}s"
    end

    [out_reader.value, err_reader.value, wait_thr.value]
  end

  format_output(command, stdout, stderr, status.exitstatus)
rescue ::Timeout::Error
  "[command timed out after #{timeout}s]: #{command}"
rescue StandardError => e
  "Error executing command: #{e.message}"
end

.execute_sandboxed(command:, timeout:, dir:) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/legion/cli/chat/tools/run_command.rb', line 44

def self.execute_sandboxed(command:, timeout:, dir:)
  timeout_ms = timeout * 1000
  result = Legion::Extensions::Exec::Runners::Shell.execute(
    command: command, cwd: dir, timeout: timeout_ms
  )

  if result[:error] == :blocked
    "Command blocked by sandbox: #{result[:reason]}"
  elsif result[:error] == :timeout
    "[command timed out after #{timeout}s]: #{command}"
  elsif result[:success] == false && result[:error]
    "Error executing command: #{result[:error]}"
  else
    format_output(command, result[:stdout], result[:stderr], result[:exit_code])
  end
rescue StandardError => e
  "Error executing command: #{e.message}"
end

.format_output(command, stdout, stderr, exit_code) ⇒ Object



87
88
89
90
91
92
93
94
# File 'lib/legion/cli/chat/tools/run_command.rb', line 87

def self.format_output(command, stdout, stderr, exit_code)
  output = String.new
  output << "$ #{command}\n"
  output << stdout.to_s unless stdout.to_s.empty?
  output << stderr.to_s unless stderr.to_s.empty?
  output << "\n[exit code: #{exit_code}]"
  output
end

.sandbox_available?Boolean

Returns:

  • (Boolean)


40
41
42
# File 'lib/legion/cli/chat/tools/run_command.rb', line 40

def self.sandbox_available?
  defined?(Legion::Extensions::Exec::Runners::Shell)
end

.sandbox_enabled?Boolean

Returns:

  • (Boolean)


34
35
36
37
38
# File 'lib/legion/cli/chat/tools/run_command.rb', line 34

def self.sandbox_enabled?
  Legion::Settings.dig(:chat, :sandboxed_commands, :enabled) == true
rescue StandardError
  false
end