Class: RailsAiContext::Tools::Validate
- Defined in:
- lib/rails_ai_context/tools/validate.rb
Constant Summary collapse
- VALID_LEVELS =
── Main entry point ─────────────────────────────────────────────
%w[syntax rails].freeze
Constants inherited from BaseTool
BaseTool::DEFAULT_SESSION, BaseTool::MAX_SESSIONS, BaseTool::MAX_SESSION_ID_LENGTH, BaseTool::SESSION_CONTEXT, BaseTool::SHARED_CACHE
Class Method Summary collapse
Methods inherited from BaseTool
abstract!, abstract?, api_only_app?, api_only_note, cache_key, cached_context, config, current_session, #dedupe_put_patch_routes, detail_param?, error_response, evict_oldest_sessions, extract_method_source_from_file, extract_method_source_from_string, find_closest_match, fuzzy_find_key, guide_row, inherited, introspection_warnings_note, invalid_detail_note, normalize_detail, not_found_response, paginate, rails_app, rails_env_name, registered_tools, reset_all_caches!, reset_cache!, session_from, session_params, session_queries, session_record, session_reset!, static_tier_banner, static_tier_refusal, text_response, touch_session, unavailable_note, with_session, with_session_for
Methods included from SectionFetch
Class Method Details
.call(files:, level: "syntax", server_context: 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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/rails_ai_context/tools/validate.rb', line 50 def self.call(files:, level: "syntax", server_context: nil) # Both spellings, because the JSON form this used to suggest is not # something the CLI accepts - following the advice produced # "file not found" with the literal brackets in the name. if files.nil? || files.empty? return text_response( "No files provided. Pass paths relative to Rails root - " \ "CLI: `--files app/models/post.rb app/models/user.rb`, " \ "MCP: `files: [\"app/models/post.rb\"]`." ) end return text_response("Too many files (#{files.size}). Maximum is #{max_files} per call.") if files.size > max_files return text_response("Unknown level: '#{level}'. Valid values: #{VALID_LEVELS.join(', ')}") unless VALID_LEVELS.include?(level) results = [] passed = 0 total = 0 files.each do |file| if file.nil? || file.strip.empty? results << "- (empty) - skipped (empty filename)" next end # Block sensitive files on the caller-supplied string BEFORE any # filesystem stat. Closes the existence-oracle side channel where # an attacker could distinguish "file not found" from "access denied" # for a path like config/master.key. Mirrors get_edit_context.rb # ordering. v5.8.1 round 2 hardening. if sensitive_file?(file) results << "\u2717 #{file} - access denied (sensitive file)" total += 1 next end full_path = rails_app.root.join(file) unless File.exist?(full_path) suggestion = find_file_suggestion(file) hint = suggestion ? " Did you mean '#{suggestion}'?" : "" results << "\u2717 #{file} - file not found.#{hint}" total += 1 next end begin real = File.realpath(full_path).to_s rails_root_real = File.realpath(rails_app.root).to_s # Separator-aware containment - matches the v5.8.1-r2 hardening in # get_view.rb / vfs.rb. Without `+ File::SEPARATOR`, a sibling-dir # like `/app/rails_evil/...` would prefix-match a Rails root at # `/app/rails`. Same bug class as the original C1. unless real == rails_root_real || real.start_with?(rails_root_real + File::SEPARATOR) results << "\u2717 #{file} - path not allowed (outside Rails root)" total += 1 next end # Defense-in-depth: re-run sensitive_file? on the resolved path. # Catches symlinks pointing into sensitive territory from a # non-sensitive caller string (e.g. app/views/leak.html.erb → # ../../config/master.key). relative_real = real.sub("#{rails_root_real}/", "") if sensitive_file?(relative_real) results << "\u2717 #{file} - access denied (resolves to sensitive file)" total += 1 next end rescue Errno::ENOENT results << "\u2717 #{file} - file not found" total += 1 next end total += 1 real_path = Pathname.new(real) ok, msg, warnings = if file.end_with?(".rb") validate_ruby(real_path) elsif file.end_with?(".html.erb") || file.end_with?(".erb") validate_erb(real_path) elsif file.end_with?(".js") validate_javascript(real_path) else results << "- #{file} - skipped (unsupported file type)" total -= 1 next end if ok results << "\u2713 #{file} - syntax OK" passed += 1 else results << "\u2717 #{file} - #{msg}" end (warnings || []).each { |w| results << " \u26A0 #{w}" } if level == "rails" && ok rails_warnings = ValidateSemantics.check_rails_semantics(file, real_path) rails_warnings.each { |w| results << " \u26A0 #{w}" } end end # Run Brakeman security scan on validated files (if installed and level:"rails") if level == "rails" brakeman_warnings = ValidateSemantics.check_brakeman_security(files) brakeman_warnings.each { |w| results << " \u26A0 #{w}" } end output = results.join("\n") output += "\n\n#{passed}/#{total} files passed" # A failed validation is the tool's core negative verdict, not # advisory guidance - flag it as an MCP error so CLI callers exit # non-zero. Mirrors the SQL-error promotion in query.rb. return error_response(output) if passed < total text_response(output) end |
.max_files ⇒ Object
17 18 19 |
# File 'lib/rails_ai_context/tools/validate.rb', line 17 def self.max_files RailsAiContext.configuration.max_validate_files end |