Class: RailsMcpServer::ExecuteRuby

Inherits:
BaseTool
  • Object
show all
Defined in:
lib/rails-mcp-server/tools/execute_ruby.rb

Constant Summary collapse

FORBIDDEN_PATTERNS =

Patterns that indicate dangerous operations

[
  # File/IO writing
  /File\.(write|open|new)\s*\([^)]*['"][wa+]/i,
  /File\.(delete|unlink|rename|chmod|chown|truncate)/i,
  /FileUtils\./i,
  /IO\.(write|syswrite|popen|pipe)/i,
  /\.(write|puts|print|syswrite)\s*[(\s]/,

  # Directory modification
  /Dir\.(mkdir|rmdir|delete|chdir)/i,

  # System/shell execution
  /system\s*[(\s]/,
  /exec\s*[(\s]/,
  /`[^`]+`/,
  /%x[{(\[]/,
  /Kernel\.(system|exec|spawn|`)/,
  /Open3\./i,
  /IO\.popen/i,
  /Process\.(spawn|exec|fork)/i,
  /Shellwords/i,

  # Network access
  /Net::(HTTP|FTP|SMTP)/i,
  /URI\.(open|parse)/i,
  /HTTParty/i,
  /Faraday/i,
  /RestClient/i,
  /open-uri/i,
  /Socket/i,
  /TCPSocket/i,
  /UDPSocket/i,

  # Dangerous Ruby features
  /eval\s*[(\s]/,
  /instance_eval/i,
  /class_eval/i,
  /module_eval/i,
  /define_method/i,
  /send\s*[(\s]+[:'"]*(system|exec|`)/i,
  /__send__/,
  /ObjectSpace/i,
  /Binding/i,
  /set_trace_func/i,

  # Environment/credentials access
  # Match any ENV usage (ENV[, ENV.fetch, ENV.to_h, ENV.values_at, ENV.each,
  # ...). Case-sensitive so it doesn't flag `Rails.env` or a local `env`.
  /\bENV\b/,
  /Rails\.application\.credentials/i,
  /Rails\.application\.secrets/i,

  # Load/require that could execute arbitrary code
  /load\s*[(\s]+[^)]*\$/i,
  /require\s+[^'"]/i
].freeze
CONFIRMATION_REQUIRED_PATTERNS =

Dual-use constructs that are NOT hard-blocked (they have legitimate read-only uses) but can defeat the static safety scan, so running them requires explicit user confirmation via confirm_risky: true. Each entry: [pattern, label, why-it-is-risky].

[
  [/(?<![.\w])open\s*\(/, "Kernel#open",
    "`open(arg)` runs a shell command when arg begins with '|', and can open network/URI targets — both escape the sandbox."],
  [/\bpublic_send\b/, "public_send",
    "dynamic dispatch can invoke methods the static scan cannot see, e.g. reaching blocked system/file APIs indirectly."],
  [/\bsend\s*[(\s]/, "send",
    "dynamic dispatch can invoke methods the static scan cannot see, e.g. reaching blocked system/file APIs indirectly."],
  [/\bconst_get\b/, "const_get",
    "resolves constants by name at runtime, which can reach classes the static scan would otherwise block."]
].freeze
SENSITIVE_PATTERNS =

Sensitive file patterns (in addition to .gitignore)

[
  /\.env(\..*)?$/i,
  /\.key$/i,
  /\.pem$/i,
  /\.crt$/i,
  /\.p12$/i,
  /credentials\.yml/i,
  /secrets\.yml/i,
  /master\.key/i,
  /config\/credentials/i,
  /config\/secrets/i,
  /\.secret$/i,
  /password/i,
  /\.ssh\//i,
  /id_rsa/i,
  /id_ed25519/i
].freeze
ALLOWED_READ_PATHS =

Read-only system data directories the sandbox may read. TZInfo lazily loads IANA timezone data on first Time.zone use; these are its default search paths plus /var/db/timezone, the real location behind macOS's /usr/share/zoneinfo symlink. Writes remain blocked by the File/Dir/ FileUtils overrides.

%w[
  /usr/share/zoneinfo
  /usr/share/lib/zoneinfo
  /etc/zoneinfo
  /var/db/timezone
].freeze
NO_OUTPUT_MESSAGE =
<<~MSG
  Code executed successfully (no output).

  Hint: Use `puts` to see results, e.g.:
    puts read_file('config/routes.rb')
    puts User.count
    puts Dir.glob('app/models/*.rb')
MSG

Instance Method Summary collapse

Instance Method Details

#call(code:, timeout: 30, confirm_risky: false) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/rails-mcp-server/tools/execute_ruby.rb', line 154

def call(code:, timeout: 30, confirm_risky: false)
  unless current_project
    return "No active project. Please switch to a project first."
  end

  timeout = [timeout.to_i, 60].min # Cap at 60 seconds
  timeout = 10 if timeout < 1

  # Step 1: Static analysis - reject outright-dangerous code
  validation_error = validate_code_safety(code)
  return validation_error if validation_error

  # Step 2: Dual-use constructs require explicit user confirmation
  unless confirm_risky
    confirmation = confirmation_required(code)
    return confirmation if confirmation
  end

  # Step 3: Build the sandboxed execution environment
  sandbox_code = build_sandbox(code)

  # Step 4: Execute with timeout
  execute_sandboxed(sandbox_code, timeout)
end