rails_ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec_status +96 -0
  3. data/AGENT_GUIDE.md +513 -0
  4. data/Appraisals +49 -0
  5. data/COMMERCIAL_LICENSE_TEMPLATE.md +92 -0
  6. data/FEATURES.md +204 -0
  7. data/LEGAL_PROTECTION_GUIDE.md +222 -0
  8. data/LICENSE +62 -0
  9. data/LICENSE_SUMMARY.md +74 -0
  10. data/MIT-LICENSE +62 -0
  11. data/PERFORMANCE.md +300 -0
  12. data/PROVIDERS.md +495 -0
  13. data/README.md +454 -0
  14. data/Rakefile +11 -0
  15. data/SPEED_OPTIMIZATIONS.md +217 -0
  16. data/STRUCTURE.md +139 -0
  17. data/USAGE_GUIDE.md +288 -0
  18. data/app/channels/ai_stream_channel.rb +33 -0
  19. data/app/components/ai/prompt_component.rb +25 -0
  20. data/app/controllers/concerns/ai/context_aware.rb +77 -0
  21. data/app/controllers/concerns/ai/streaming.rb +41 -0
  22. data/app/helpers/ai_helper.rb +164 -0
  23. data/app/jobs/ai/generate_embedding_job.rb +25 -0
  24. data/app/jobs/ai/generate_summary_job.rb +25 -0
  25. data/app/models/concerns/ai/embeddable.rb +38 -0
  26. data/app/views/rails_ai/dashboard/index.html.erb +51 -0
  27. data/config/routes.rb +19 -0
  28. data/lib/generators/rails_ai/install/install_generator.rb +38 -0
  29. data/lib/rails_ai/agents/agent_manager.rb +258 -0
  30. data/lib/rails_ai/agents/agent_team.rb +243 -0
  31. data/lib/rails_ai/agents/base_agent.rb +331 -0
  32. data/lib/rails_ai/agents/collaboration.rb +238 -0
  33. data/lib/rails_ai/agents/memory.rb +116 -0
  34. data/lib/rails_ai/agents/message_bus.rb +95 -0
  35. data/lib/rails_ai/agents/specialized_agents.rb +391 -0
  36. data/lib/rails_ai/agents/task_queue.rb +111 -0
  37. data/lib/rails_ai/cache.rb +14 -0
  38. data/lib/rails_ai/config.rb +40 -0
  39. data/lib/rails_ai/context.rb +7 -0
  40. data/lib/rails_ai/context_analyzer.rb +86 -0
  41. data/lib/rails_ai/engine.rb +48 -0
  42. data/lib/rails_ai/events.rb +9 -0
  43. data/lib/rails_ai/image_context.rb +110 -0
  44. data/lib/rails_ai/performance.rb +231 -0
  45. data/lib/rails_ai/provider.rb +8 -0
  46. data/lib/rails_ai/providers/anthropic_adapter.rb +256 -0
  47. data/lib/rails_ai/providers/base.rb +60 -0
  48. data/lib/rails_ai/providers/dummy_adapter.rb +29 -0
  49. data/lib/rails_ai/providers/gemini_adapter.rb +509 -0
  50. data/lib/rails_ai/providers/openai_adapter.rb +535 -0
  51. data/lib/rails_ai/providers/secure_anthropic_adapter.rb +206 -0
  52. data/lib/rails_ai/providers/secure_openai_adapter.rb +284 -0
  53. data/lib/rails_ai/railtie.rb +48 -0
  54. data/lib/rails_ai/redactor.rb +12 -0
  55. data/lib/rails_ai/security/api_key_manager.rb +82 -0
  56. data/lib/rails_ai/security/audit_logger.rb +46 -0
  57. data/lib/rails_ai/security/error_handler.rb +62 -0
  58. data/lib/rails_ai/security/input_validator.rb +176 -0
  59. data/lib/rails_ai/security/secure_file_handler.rb +45 -0
  60. data/lib/rails_ai/security/secure_http_client.rb +177 -0
  61. data/lib/rails_ai/security.rb +0 -0
  62. data/lib/rails_ai/version.rb +5 -0
  63. data/lib/rails_ai/window_context.rb +103 -0
  64. data/lib/rails_ai.rb +502 -0
  65. data/monitoring/ci_setup_guide.md +214 -0
  66. data/monitoring/enhanced_monitoring_script.rb +237 -0
  67. data/monitoring/google_alerts_setup.md +42 -0
  68. data/monitoring_log_20250921.txt +0 -0
  69. data/monitoring_script.rb +161 -0
  70. data/rails_ai.gemspec +54 -0
  71. data/scripts/security_scanner.rb +353 -0
  72. data/setup_monitoring.sh +163 -0
  73. data/wiki/API-Documentation.md +734 -0
  74. data/wiki/Architecture-Overview.md +672 -0
  75. data/wiki/Contributing-Guide.md +407 -0
  76. data/wiki/Development-Setup.md +532 -0
  77. data/wiki/Home.md +278 -0
  78. data/wiki/Installation-Guide.md +527 -0
  79. data/wiki/Quick-Start.md +186 -0
  80. data/wiki/README.md +135 -0
  81. data/wiki/Release-Process.md +467 -0
  82. metadata +385 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ module Security
5
+ class ErrorHandler
6
+ def self.handle_security_error(error, context = {})
7
+ # Log security error
8
+ AuditLogger.log_security_event(:security_error, {
9
+ error_class: error.class.name,
10
+ error_message: error.message,
11
+ context: context,
12
+ timestamp: Time.now
13
+ })
14
+
15
+ # Return sanitized error message
16
+ sanitized_message = sanitize_error_message(error)
17
+
18
+ # In production, don't expose internal details
19
+ if Rails.env.production?
20
+ case error
21
+ when ValidationError
22
+ "Invalid input provided"
23
+ when RateLimitError
24
+ "Rate limit exceeded. Please try again later"
25
+ when SecurityError
26
+ "Security error occurred"
27
+ else
28
+ "An error occurred while processing your request"
29
+ end
30
+ else
31
+ sanitized_message
32
+ end
33
+ end
34
+
35
+ def self.sanitize_error_message(error)
36
+ message = error.message.to_s
37
+
38
+ # Remove sensitive information
39
+ message = message.gsub(/api[_-]?key/i, 'API_KEY')
40
+ message = message.gsub(/password/i, 'PASSWORD')
41
+ message = message.gsub(/secret/i, 'SECRET')
42
+ message = message.gsub(/token/i, 'TOKEN')
43
+
44
+ # Remove file paths
45
+ message = message.gsub(%r{/[a-zA-Z0-9_/.-]+}, '[PATH]')
46
+
47
+ # Remove IP addresses
48
+ message = message.gsub(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, '[IP]')
49
+
50
+ # Remove URLs
51
+ message = message.gsub(%r{https?://[^\s]+}, '[URL]')
52
+
53
+ message
54
+ end
55
+
56
+ def self.log_and_raise(error, context = {})
57
+ handle_security_error(error, context)
58
+ raise error
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'ipaddr'
5
+
6
+ module RailsAi
7
+ module Security
8
+ class InputValidator
9
+ MAX_FILE_SIZE = 50.megabytes
10
+ ALLOWED_IMAGE_TYPES = %w[image/jpeg image/png image/gif image/webp].freeze
11
+ ALLOWED_VIDEO_TYPES = %w[video/mp4 video/webm video/quicktime].freeze
12
+ ALLOWED_AUDIO_TYPES = %w[audio/mpeg audio/wav audio/ogg audio/mp4].freeze
13
+
14
+ ALLOWED_SCHEMES = %w[http https].freeze
15
+ BLOCKED_HOSTS = %w[
16
+ localhost 127.0.0.1 0.0.0.0 ::1
17
+ 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
18
+ metadata.google.internal
19
+ instance-data.ec2.internal
20
+ ].freeze
21
+
22
+ def self.validate_text_input(text, max_length: 10000)
23
+ raise ValidationError, "Text cannot be nil" if text.nil?
24
+
25
+ text = text.to_s.strip
26
+ raise ValidationError, "Text cannot be empty" if text.empty?
27
+ raise ValidationError, "Text too long (max: #{max_length})" if text.length > max_length
28
+
29
+ if text.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/)
30
+ raise ValidationError, "Text contains invalid characters"
31
+ end
32
+
33
+ text
34
+ end
35
+
36
+ def self.validate_file_path(path)
37
+ raise ValidationError, "File path cannot be nil" if path.nil?
38
+
39
+ path = File.expand_path(path)
40
+
41
+ if path.include?('..') || path.include?('~')
42
+ raise ValidationError, "Invalid file path: directory traversal detected"
43
+ end
44
+
45
+ unless File.exist?(path) && File.readable?(path)
46
+ raise ValidationError, "File not found or not readable: #{path}"
47
+ end
48
+
49
+ if File.size(path) > MAX_FILE_SIZE
50
+ raise ValidationError, "File too large (max: #{MAX_FILE_SIZE} bytes)"
51
+ end
52
+
53
+ path
54
+ end
55
+
56
+ def self.validate_base64_data(data, expected_type: nil)
57
+ raise ValidationError, "Base64 data cannot be nil" if data.nil?
58
+
59
+ unless data.match?(/\A[A-Za-z0-9+\/]*={0,2}\z/)
60
+ raise ValidationError, "Invalid base64 format"
61
+ end
62
+
63
+ begin
64
+ decoded = Base64.strict_decode64(data)
65
+ if decoded.size > MAX_FILE_SIZE
66
+ raise ValidationError, "Base64 data too large (max: #{MAX_FILE_SIZE} bytes)"
67
+ end
68
+ rescue ArgumentError
69
+ raise ValidationError, "Invalid base64 data"
70
+ end
71
+
72
+ if expected_type && data.start_with?("data:")
73
+ mime_type = data.split(';')[0].split(':')[1]
74
+ unless valid_mime_type?(mime_type, expected_type)
75
+ raise ValidationError, "Invalid MIME type: #{mime_type}"
76
+ end
77
+ end
78
+
79
+ data
80
+ end
81
+
82
+ def self.validate_url(url)
83
+ raise ValidationError, "URL cannot be nil" if url.nil?
84
+
85
+ begin
86
+ uri = URI.parse(url)
87
+ rescue URI::InvalidURIError
88
+ raise ValidationError, "Invalid URL format"
89
+ end
90
+
91
+ unless ALLOWED_SCHEMES.include?(uri.scheme)
92
+ raise ValidationError, "Invalid URL scheme: #{uri.scheme}"
93
+ end
94
+
95
+ if blocked_host?(uri.host)
96
+ raise ValidationError, "URL blocked for security reasons"
97
+ end
98
+
99
+ if private_ip?(uri.host)
100
+ raise ValidationError, "Private IP addresses not allowed"
101
+ end
102
+
103
+ url
104
+ end
105
+
106
+ def self.validate_messages(messages)
107
+ raise ValidationError, "Messages cannot be nil" if messages.nil?
108
+ raise ValidationError, "Messages must be an array" unless messages.is_a?(Array)
109
+ raise ValidationError, "Messages cannot be empty" if messages.empty?
110
+ raise ValidationError, "Too many messages (max: 100)" if messages.length > 100
111
+
112
+ messages.each_with_index do |message, index|
113
+ validate_message(message, index)
114
+ end
115
+
116
+ messages
117
+ end
118
+
119
+ private
120
+
121
+ def self.validate_message(message, index)
122
+ raise ValidationError, "Message #{index} must be a hash" unless message.is_a?(Hash)
123
+ raise ValidationError, "Message #{index} missing role" unless message[:role]
124
+ raise ValidationError, "Message #{index} missing content" unless message[:content]
125
+
126
+ valid_roles = %w[system user assistant]
127
+ unless valid_roles.include?(message[:role])
128
+ raise ValidationError, "Message #{index} has invalid role: #{message[:role]}"
129
+ end
130
+
131
+ validate_text_input(message[:content], max_length: 50000)
132
+ end
133
+
134
+ def self.valid_mime_type?(mime_type, expected_type)
135
+ case expected_type
136
+ when :image
137
+ ALLOWED_IMAGE_TYPES.include?(mime_type)
138
+ when :video
139
+ ALLOWED_VIDEO_TYPES.include?(mime_type)
140
+ when :audio
141
+ ALLOWED_AUDIO_TYPES.include?(mime_type)
142
+ else
143
+ true
144
+ end
145
+ end
146
+
147
+ def self.blocked_host?(host)
148
+ return true if host.nil?
149
+
150
+ BLOCKED_HOSTS.any? do |blocked|
151
+ if blocked.include?('/')
152
+ begin
153
+ blocked_network = IPAddr.new(blocked)
154
+ blocked_network.include?(host)
155
+ rescue IPAddr::InvalidAddressError
156
+ false
157
+ end
158
+ else
159
+ host == blocked || host.end_with?(".#{blocked}")
160
+ end
161
+ end
162
+ end
163
+
164
+ def self.private_ip?(host)
165
+ return false if host.nil?
166
+
167
+ begin
168
+ ip = IPAddr.new(host)
169
+ ip.private? || ip.loopback?
170
+ rescue IPAddr::InvalidAddressError
171
+ false
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module RailsAi
6
+ module Security
7
+ class SecureFileHandler
8
+ def self.safe_file_read(path)
9
+ validate_path = InputValidator.validate_file_path(path)
10
+
11
+ File.open(validate_path, 'rb') do |file|
12
+ content = file.read(InputValidator::MAX_FILE_SIZE)
13
+ if file.eof?
14
+ content
15
+ else
16
+ raise ValidationError, "File too large"
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.safe_temp_file(content, extension = '.tmp')
22
+ temp_file = Tempfile.new(['rails_ai', extension])
23
+ temp_file.binmode
24
+ temp_file.write(content)
25
+ temp_file.rewind
26
+ temp_file
27
+ end
28
+
29
+ def self.safe_file_size(path)
30
+ File.size(InputValidator.validate_file_path(path))
31
+ end
32
+
33
+ def self.safe_file_exists?(path)
34
+ return false if path.nil?
35
+
36
+ begin
37
+ InputValidator.validate_file_path(path)
38
+ true
39
+ rescue ValidationError
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'timeout'
5
+ require 'uri'
6
+
7
+ module RailsAi
8
+ module Security
9
+ class SecureHTTPClient
10
+ DEFAULT_TIMEOUT = 30
11
+ MAX_REDIRECTS = 5
12
+
13
+ def self.make_request(url, options = {})
14
+ uri = URI.parse(url)
15
+
16
+ # Security validations
17
+ validate_url(uri)
18
+
19
+ http = create_secure_http_client(uri)
20
+ request = create_secure_request(uri, options)
21
+
22
+ # Set security headers
23
+ set_security_headers(request)
24
+
25
+ # Execute request with timeout
26
+ execute_with_timeout(http, request, options[:timeout] || DEFAULT_TIMEOUT)
27
+ end
28
+
29
+ def self.make_streaming_request(url, options = {}, &block)
30
+ uri = URI.parse(url)
31
+
32
+ # Security validations
33
+ validate_url(uri)
34
+
35
+ http = create_secure_http_client(uri)
36
+ request = create_secure_request(uri, options)
37
+
38
+ # Set security headers
39
+ set_security_headers(request)
40
+
41
+ # Execute streaming request
42
+ execute_streaming_request(http, request, options[:timeout] || DEFAULT_TIMEOUT, &block)
43
+ end
44
+
45
+ private
46
+
47
+ def self.validate_url(uri)
48
+ unless %w[http https].include?(uri.scheme)
49
+ raise SecurityError, "Invalid URL scheme: #{uri.scheme}"
50
+ end
51
+
52
+ if blocked_host?(uri.host)
53
+ raise SecurityError, "URL blocked for security reasons: #{uri.host}"
54
+ end
55
+
56
+ if private_ip?(uri.host)
57
+ raise SecurityError, "Private IP addresses not allowed: #{uri.host}"
58
+ end
59
+ end
60
+
61
+ def self.create_secure_http_client(uri)
62
+ http = Net::HTTP.new(uri.host, uri.port)
63
+ http.use_ssl = (uri.scheme == 'https')
64
+
65
+ # Security settings
66
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
67
+ http.verify_depth = 5
68
+
69
+ # Timeout settings
70
+ http.read_timeout = DEFAULT_TIMEOUT
71
+ http.open_timeout = 10
72
+ http.ssl_timeout = 10
73
+
74
+ # Disable SSL compression
75
+ http.ssl_version = :TLSv1_2
76
+
77
+ http
78
+ end
79
+
80
+ def self.create_secure_request(uri, options)
81
+ case options[:method]&.upcase || 'GET'
82
+ when 'GET'
83
+ Net::HTTP::Get.new(uri)
84
+ when 'POST'
85
+ request = Net::HTTP::Post.new(uri)
86
+ request.body = options[:body] if options[:body]
87
+ request
88
+ when 'PUT'
89
+ request = Net::HTTP::Put.new(uri)
90
+ request.body = options[:body] if options[:body]
91
+ request
92
+ else
93
+ raise SecurityError, "Unsupported HTTP method: #{options[:method]}"
94
+ end
95
+ end
96
+
97
+ def self.set_security_headers(request)
98
+ request['User-Agent'] = 'RailsAI/1.0'
99
+ request['Accept'] = 'application/json'
100
+ request['Connection'] = 'close'
101
+ request['Cache-Control'] = 'no-cache'
102
+
103
+ # Security headers
104
+ request['X-Requested-With'] = 'RailsAI'
105
+ request['X-Content-Type-Options'] = 'nosniff'
106
+ request['X-Frame-Options'] = 'DENY'
107
+ request['X-XSS-Protection'] = '1; mode=block'
108
+ end
109
+
110
+ def self.execute_with_timeout(http, request, timeout)
111
+ Timeout.timeout(timeout) do
112
+ response = http.request(request)
113
+ validate_response(response)
114
+ response
115
+ end
116
+ rescue Timeout::Error
117
+ raise SecurityError, "Request timeout after #{timeout} seconds"
118
+ rescue => e
119
+ raise SecurityError, "HTTP request failed: #{e.message}"
120
+ end
121
+
122
+ def self.execute_streaming_request(http, request, timeout, &block)
123
+ Timeout.timeout(timeout) do
124
+ http.request(request) do |response|
125
+ validate_response(response)
126
+
127
+ response.read_body do |chunk|
128
+ block.call(chunk) if block
129
+ end
130
+ end
131
+ end
132
+ rescue Timeout::Error
133
+ raise SecurityError, "Streaming request timeout after #{timeout} seconds"
134
+ rescue => e
135
+ raise SecurityError, "Streaming request failed: #{e.message}"
136
+ end
137
+
138
+ def self.validate_response(response)
139
+ case response.code.to_i
140
+ when 200..299
141
+ response
142
+ when 400..499
143
+ raise SecurityError, "Client error: #{response.code} #{response.message}"
144
+ when 500..599
145
+ raise SecurityError, "Server error: #{response.code} #{response.message}"
146
+ else
147
+ raise SecurityError, "Unexpected response: #{response.code} #{response.message}"
148
+ end
149
+ end
150
+
151
+ def self.blocked_host?(host)
152
+ return true if host.nil?
153
+
154
+ blocked_hosts = %w[
155
+ localhost 127.0.0.1 0.0.0.0 ::1
156
+ metadata.google.internal
157
+ instance-data.ec2.internal
158
+ 169.254.169.254
159
+ ]
160
+
161
+ blocked_hosts.any? { |blocked| host == blocked || host.end_with?(".#{blocked}") }
162
+ end
163
+
164
+ def self.private_ip?(host)
165
+ return false if host.nil?
166
+
167
+ require 'ipaddr'
168
+ begin
169
+ ip = IPAddr.new(host)
170
+ ip.private? || ip.loopback? || ip.link_local?
171
+ rescue IPAddr::InvalidAddressError
172
+ false
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
Binary file
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ class WindowContext
5
+ attr_reader :controller, :action, :params, :request, :session, :cookies
6
+
7
+ def initialize(controller: nil, action: nil, params: {}, request: nil, session: {}, cookies: {})
8
+ @controller = controller
9
+ @action = action
10
+ @params = params || {}
11
+ @request = request
12
+ @session = session || {}
13
+ @cookies = cookies || {}
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ controller: controller_name,
19
+ action: action_name,
20
+ route: route_info,
21
+ params: sanitized_params,
22
+ user_agent: user_agent,
23
+ referer: referer,
24
+ ip_address: ip_address,
25
+ session_data: sanitized_session,
26
+ cookies: sanitized_cookies,
27
+ timestamp: current_time.iso8601
28
+ }
29
+ end
30
+
31
+ def self.from_controller(controller)
32
+ new(
33
+ controller: controller.class.name,
34
+ action: controller.action_name,
35
+ params: extract_params(controller),
36
+ request: controller.request,
37
+ session: controller.session.to_h,
38
+ cookies: controller.cookies.to_h
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ def current_time
45
+ if defined?(Time.current)
46
+ Time.current
47
+ else
48
+ Time.now
49
+ end
50
+ end
51
+
52
+ def self.extract_params(controller)
53
+ if controller.params.respond_to?(:to_unsafe_h)
54
+ controller.params.to_unsafe_h
55
+ elsif controller.params.respond_to?(:to_h)
56
+ controller.params.to_h
57
+ else
58
+ controller.params
59
+ end
60
+ end
61
+
62
+ def controller_name
63
+ controller&.class&.name || 'Unknown'
64
+ end
65
+
66
+ def action_name
67
+ action || 'Unknown'
68
+ end
69
+
70
+ def route_info
71
+ return 'Unknown' unless request
72
+
73
+ "#{request.method} #{request.path}"
74
+ end
75
+
76
+ def sanitized_params
77
+ # Remove sensitive parameters
78
+ params.except('password', 'password_confirmation', 'token', 'secret', 'key')
79
+ end
80
+
81
+ def user_agent
82
+ request&.user_agent || 'Unknown'
83
+ end
84
+
85
+ def referer
86
+ request&.referer || 'Direct'
87
+ end
88
+
89
+ def ip_address
90
+ request&.remote_ip || 'Unknown'
91
+ end
92
+
93
+ def sanitized_session
94
+ # Remove sensitive session data
95
+ session.except('password', 'token', 'secret', 'key', 'csrf_token')
96
+ end
97
+
98
+ def sanitized_cookies
99
+ # Remove sensitive cookies
100
+ cookies.except('_session_id', 'csrf_token', 'remember_token')
101
+ end
102
+ end
103
+ end