Class: RailsActiveMcp::SafetyChecker

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_active_mcp/safety_checker.rb

Constant Summary collapse

DANGEROUS_PATTERNS =
[
  { pattern: /\.delete_all\b/, description: 'Mass deletion of records', severity: :high },
  { pattern: /\.destroy_all\b/, description: 'Mass destruction of records', severity: :high },
  { pattern: /\.drop\b/, description: 'Database table dropping', severity: :critical },
  { pattern: /system\s*\(/, description: 'System command execution', severity: :critical },
  { pattern: /Kernel\.system\s*\(/, description: 'Kernel system command execution', severity: :critical },
  { pattern: /exec\s*\(/, description: 'Process execution', severity: :critical },
  { pattern: /`[^`]*`/, description: 'Shell command execution', severity: :critical },
  { pattern: /File\.delete/, description: 'File deletion', severity: :high },
  { pattern: /FileUtils\./, description: 'File system operations', severity: :high },
  { pattern: /Dir\.delete/, description: 'Directory deletion', severity: :high },
  { pattern: /ActiveRecord::Base\.connection\.execute/, description: 'Raw SQL execution', severity: :medium },
  { pattern: /\.update_all\(/, description: 'Mass update without callbacks', severity: :medium },
  { pattern: /eval\s*\(/, description: 'Dynamic code evaluation', severity: :high },
  { pattern: /send\s*\(/, description: 'Dynamic method calling', severity: :high },
  { pattern: /__send__\s*\(/, description: 'Dynamic method calling (bypass)', severity: :high },
  { pattern: /public_send\s*\(/, description: 'Dynamic public method calling', severity: :high },
  { pattern: /const_set/, description: 'Dynamic constant definition', severity: :medium },
  { pattern: /remove_const/, description: 'Constant removal', severity: :high },
  { pattern: /undef_method/, description: 'Method removal', severity: :high },
  { pattern: /alias_method/, description: 'Method aliasing', severity: :medium },
  { pattern: /load\s*\(/, description: 'Code loading', severity: :medium },
  { pattern: /require\s*\(/, description: 'Library requiring', severity: :low },
  { pattern: /\bexit\b/, description: 'Process termination', severity: :high },
  { pattern: /\babort\b/, description: 'Process abortion', severity: :high },
  { pattern: /\bfork\b/, description: 'Process forking', severity: :high },
  { pattern: /Thread\.new/, description: 'Thread creation', severity: :medium },
  { pattern: /\$LOAD_PATH/, description: 'Load path manipulation', severity: :medium },
  { pattern: /ENV\[/, description: 'Environment variable access', severity: :low },
  { pattern: /Rails\.env\s*=/, description: 'Environment changing', severity: :high },
  { pattern: /Rails\.application\.secrets/, description: 'Secrets access', severity: :medium }
].freeze
READ_ONLY_PATTERNS =
[
  /\.(find|find_by|find_each|find_in_batches)\b/,
  /\.(where|all|first|last|take)\b/,
  /\.(count|sum|average|maximum|minimum|size|length)\b/,
  /\.(pluck|ids|exists\?|empty\?|any\?|many\?)\b/,
  /\.(select|distinct|group|order|limit|offset)\b/,
  /\.(includes|joins|left_joins|preload|eager_load)\b/,
  /\.(to_a|to_sql|explain|inspect|as_json|to_json)\b/,
  /\.(attributes|attribute_names|column_names)\b/,
  /\.model_name\b/,
  /\.table_name\b/,
  /\.primary_key\b/,
  /\.connection\.schema_cache/,
  /Rails\.(env|root|application\.class|version)/
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ SafetyChecker

Returns a new instance of SafetyChecker.



54
55
56
# File 'lib/rails_active_mcp/safety_checker.rb', line 54

def initialize(config)
  @config = config
end

Instance Method Details

#analyze(code) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rails_active_mcp/safety_checker.rb', line 63

def analyze(code)
  violations = []

  # Check against dangerous patterns
  dangerous_patterns.each do |pattern_info|
    violations << pattern_info if code.match?(pattern_info[:pattern])
  end

  # Determine if code is read-only
  read_only = read_only?(code)

  # Calculate safety
  critical_violations = violations.select { |v| v[:severity] == :critical }
  high_violations = violations.select { |v| v[:severity] == :high }

  # In safe mode: allow code with no critical/high violations
  # For database operations, also require read_only patterns
  # For simple Ruby code (no database patterns), allow if no dangerous violations
  if @config.safe_mode
    has_database_operations = code.match?(/\b(User|Model|ActiveRecord|\.where|\.find|\.create|\.update|\.delete)\b/)
    safe = if has_database_operations
             read_only && critical_violations.empty? && high_violations.empty?
           else
             # Simple Ruby code - just check for dangerous patterns
             critical_violations.empty? && high_violations.empty?
           end
  else
    # Not in safe mode - only block critical violations
    safe = critical_violations.empty?
  end

  {
    safe: safe,
    read_only: read_only,
    violations: violations,
    summary: generate_summary(violations, read_only)
  }
end

#read_only?(code) ⇒ Boolean

Returns:

  • (Boolean)


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rails_active_mcp/safety_checker.rb', line 102

def read_only?(code)
  # Must contain at least one read-only pattern
  has_read_only = READ_ONLY_PATTERNS.any? { |pattern| code.match?(pattern) }

  # Must not contain any obvious mutation patterns
  mutation_patterns = [
    /\.(save|create|update|delete|destroy)\b/,
    /\.(save!|create!|update!|delete!|destroy!)\b/,
    /\.reload\b/,
    /\.transaction\b/,
    /=\s*[^=]/ # Assignment (basic check)
  ]

  has_mutations = mutation_patterns.any? { |pattern| code.match?(pattern) }

  has_read_only && !has_mutations
end

#safe?(code) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
61
# File 'lib/rails_active_mcp/safety_checker.rb', line 58

def safe?(code)
  analysis = analyze(code)
  analysis[:safe]
end