chaos_to_the_rescue 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.
- checksums.yaml +7 -0
- data/.claude/CLAUDE.md +3 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +8 -0
- data/.claude/settings.local.json +9 -0
- data/.devcontainer/.env.example +12 -0
- data/.devcontainer/Dockerfile.standalone +30 -0
- data/.devcontainer/README.md +207 -0
- data/.devcontainer/devcontainer.json +54 -0
- data/.devcontainer/docker-compose.yml +32 -0
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/FEATURES.md +306 -0
- data/LICENSE.txt +21 -0
- data/README.md +502 -0
- data/Rakefile +10 -0
- data/examples/guidance_and_verification.rb +226 -0
- data/lib/chaos_to_the_rescue/chaos_rescue.rb +336 -0
- data/lib/chaos_to_the_rescue/configuration.rb +157 -0
- data/lib/chaos_to_the_rescue/llm_client.rb +182 -0
- data/lib/chaos_to_the_rescue/logger.rb +82 -0
- data/lib/chaos_to_the_rescue/railtie.rb +24 -0
- data/lib/chaos_to_the_rescue/redactor.rb +76 -0
- data/lib/chaos_to_the_rescue/rescue_from.rb +156 -0
- data/lib/chaos_to_the_rescue/verifier.rb +277 -0
- data/lib/chaos_to_the_rescue/version.rb +5 -0
- data/lib/chaos_to_the_rescue.rb +57 -0
- data/lib/generators/chaos_to_the_rescue/install_generator.rb +37 -0
- data/lib/generators/chaos_to_the_rescue/templates/chaos_to_the_rescue.rb +97 -0
- data/sig/chaos_to_the_rescue.rbs +4 -0
- metadata +89 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module ChaosToTheRescue
|
|
6
|
+
# Thread-safe configuration for ChaosToTheRescue gem
|
|
7
|
+
class Configuration
|
|
8
|
+
include MonitorMixin
|
|
9
|
+
|
|
10
|
+
# @return [Boolean] Whether the gem is enabled globally (default: false)
|
|
11
|
+
attr_accessor :enabled
|
|
12
|
+
|
|
13
|
+
# @return [Boolean] Whether to automatically define generated methods (default: false)
|
|
14
|
+
attr_accessor :auto_define_methods
|
|
15
|
+
|
|
16
|
+
# @return [Array<Regexp>] Patterns for allowed method names
|
|
17
|
+
attr_accessor :allowed_method_name_patterns
|
|
18
|
+
|
|
19
|
+
# @return [Array<String, Symbol>] Explicit allowlist of method names
|
|
20
|
+
attr_accessor :allowlist
|
|
21
|
+
|
|
22
|
+
# @return [String] LLM model to use (default: "gpt-4o-mini")
|
|
23
|
+
attr_accessor :model
|
|
24
|
+
|
|
25
|
+
# @return [Integer] Maximum characters to send in prompts (default: 8000)
|
|
26
|
+
attr_accessor :max_prompt_chars
|
|
27
|
+
|
|
28
|
+
# @return [Symbol] Log level (:debug, :info, :warn, :error, :fatal, :unknown)
|
|
29
|
+
attr_accessor :log_level
|
|
30
|
+
|
|
31
|
+
# @return [Array<Regexp>] Patterns to redact from prompts
|
|
32
|
+
attr_accessor :redact_patterns
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] Whether to log suggestions to file (default: true)
|
|
35
|
+
attr_accessor :log_suggestions
|
|
36
|
+
|
|
37
|
+
# @return [String] Path for suggestion log file
|
|
38
|
+
attr_accessor :suggestions_log_path
|
|
39
|
+
|
|
40
|
+
# @return [String] OpenAI API key (optional, can also use ENV['OPENAI_API_KEY'])
|
|
41
|
+
attr_accessor :openai_api_key
|
|
42
|
+
|
|
43
|
+
# @return [String] Anthropic API key (optional, can also use ENV['ANTHROPIC_API_KEY'])
|
|
44
|
+
attr_accessor :anthropic_api_key
|
|
45
|
+
|
|
46
|
+
# @return [Hash] Global method guidance for LLM-generated methods
|
|
47
|
+
attr_accessor :method_guidance
|
|
48
|
+
|
|
49
|
+
def initialize
|
|
50
|
+
super # Initialize MonitorMixin
|
|
51
|
+
|
|
52
|
+
# Safe defaults - disabled by default
|
|
53
|
+
@enabled = false
|
|
54
|
+
@auto_define_methods = false
|
|
55
|
+
@allowed_method_name_patterns = []
|
|
56
|
+
@allowlist = []
|
|
57
|
+
@model = "gpt-5-mini"
|
|
58
|
+
@max_prompt_chars = 8000
|
|
59
|
+
@log_level = :info
|
|
60
|
+
@log_suggestions = true
|
|
61
|
+
@suggestions_log_path = "tmp/chaos_to_the_rescue_suggestions.log"
|
|
62
|
+
@redact_patterns = default_redact_patterns
|
|
63
|
+
@openai_api_key = nil
|
|
64
|
+
@anthropic_api_key = nil
|
|
65
|
+
@method_guidance = {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Thread-safe configuration block
|
|
69
|
+
# @yield [Configuration] the configuration object
|
|
70
|
+
def configure
|
|
71
|
+
synchronize do
|
|
72
|
+
yield self
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if a method name is allowed based on patterns and allowlist
|
|
77
|
+
# @param method_name [String, Symbol] the method name to check
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def method_allowed?(method_name)
|
|
80
|
+
method_str = method_name.to_s
|
|
81
|
+
method_sym = method_name.to_sym
|
|
82
|
+
|
|
83
|
+
# Check explicit allowlist
|
|
84
|
+
return true if allowlist.include?(method_str) || allowlist.include?(method_sym)
|
|
85
|
+
|
|
86
|
+
# Check patterns
|
|
87
|
+
allowed_method_name_patterns.any? { |pattern| pattern.match?(method_str) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Allow all method names by adding a catch-all pattern
|
|
91
|
+
# WARNING: Use with caution - this allows ChaosRescue to intercept any method call
|
|
92
|
+
# @return [void]
|
|
93
|
+
def allow_everything!
|
|
94
|
+
synchronize do
|
|
95
|
+
@allowed_method_name_patterns = [/.*/]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Reset configuration to defaults
|
|
100
|
+
def reset!
|
|
101
|
+
synchronize do
|
|
102
|
+
@enabled = false
|
|
103
|
+
@auto_define_methods = false
|
|
104
|
+
@allowed_method_name_patterns = []
|
|
105
|
+
@allowlist = []
|
|
106
|
+
@model = "gpt-5-mini"
|
|
107
|
+
@max_prompt_chars = 8000
|
|
108
|
+
@log_level = :info
|
|
109
|
+
@log_suggestions = true
|
|
110
|
+
@suggestions_log_path = "tmp/chaos_to_the_rescue_suggestions.log"
|
|
111
|
+
@redact_patterns = default_redact_patterns
|
|
112
|
+
@openai_api_key = nil
|
|
113
|
+
@anthropic_api_key = nil
|
|
114
|
+
@method_guidance = {}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def default_redact_patterns
|
|
121
|
+
[
|
|
122
|
+
# Environment variable references
|
|
123
|
+
/ENV\[['"]([^'"]+)['"]\]/i,
|
|
124
|
+
/ENV\.fetch\(['"]+([^'"]+)['"]+.*?\)/i,
|
|
125
|
+
|
|
126
|
+
# AWS credentials
|
|
127
|
+
/AWS_ACCESS_KEY_ID[=:]\s*['"]?([A-Z0-9]{20})['"]?/i,
|
|
128
|
+
/AWS_SECRET_ACCESS_KEY[=:]\s*['"]?([A-Za-z0-9\/+=]{40})['"]?/i,
|
|
129
|
+
/AKIA[0-9A-Z]{16}/,
|
|
130
|
+
|
|
131
|
+
# OpenAI tokens
|
|
132
|
+
/sk-[a-zA-Z0-9]{20,}/,
|
|
133
|
+
/OPENAI_API_KEY[=:]\s*['"]?([^'"\s]+)['"]?/i,
|
|
134
|
+
|
|
135
|
+
# GitHub tokens
|
|
136
|
+
/ghp_[a-zA-Z0-9]{20,}/,
|
|
137
|
+
/gho_[a-zA-Z0-9]{20,}/,
|
|
138
|
+
/ghu_[a-zA-Z0-9]{20,}/,
|
|
139
|
+
/ghs_[a-zA-Z0-9]{20,}/,
|
|
140
|
+
/ghr_[a-zA-Z0-9]{20,}/,
|
|
141
|
+
/GITHUB_TOKEN[=:]\s*['"]?([^'"\s]+)['"]?/i,
|
|
142
|
+
|
|
143
|
+
# Rails credentials
|
|
144
|
+
/Rails\.application\.credentials\.[a-z_]+/i,
|
|
145
|
+
|
|
146
|
+
# Generic secrets
|
|
147
|
+
/API_KEY[=:]\s*['"]?([^'"\s]+)['"]?/i,
|
|
148
|
+
/SECRET[=:]\s*['"]?([^'"\s]+)['"]?/i,
|
|
149
|
+
/TOKEN[=:]\s*['"]?([^'"\s]+)['"]?/i,
|
|
150
|
+
/PASSWORD[=:]\s*['"]?([^'"\s]+)['"]?/i,
|
|
151
|
+
|
|
152
|
+
# Private keys
|
|
153
|
+
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/i
|
|
154
|
+
]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChaosToTheRescue
|
|
4
|
+
# Abstract interface for LLM interactions
|
|
5
|
+
class LLMClient
|
|
6
|
+
class LLMNotAvailableError < StandardError; end
|
|
7
|
+
class LLMRequestError < StandardError; end
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@redactor = Redactor.new
|
|
11
|
+
check_llm_availability!
|
|
12
|
+
configure_ruby_llm!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate Ruby method code based on context
|
|
16
|
+
# @param context [Hash] context including :method_name, :class_name, :args, etc.
|
|
17
|
+
# @return [Hash] with keys: :method_name, :ruby_code, :explanation
|
|
18
|
+
def generate_method_code(context)
|
|
19
|
+
prompt = build_method_generation_prompt(context)
|
|
20
|
+
redacted_prompt = truncate_prompt(@redactor.redact(prompt))
|
|
21
|
+
|
|
22
|
+
ChaosToTheRescue.logger.debug("Generating method code for #{context[:method_name]}")
|
|
23
|
+
|
|
24
|
+
response = call_llm(redacted_prompt)
|
|
25
|
+
parse_method_generation_response(response, context[:method_name])
|
|
26
|
+
rescue => e
|
|
27
|
+
ChaosToTheRescue.logger.error("Failed to generate method code: #{e.message}")
|
|
28
|
+
raise LLMRequestError, "Failed to generate method code: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Suggest a fix for an exception
|
|
32
|
+
# @param context [Hash] context including :exception, :backtrace, :guidance, etc.
|
|
33
|
+
# @return [Hash] with keys: :title, :diagnosis, :proposed_changes, :files_to_edit
|
|
34
|
+
def suggest_fix(context)
|
|
35
|
+
prompt = build_fix_suggestion_prompt(context)
|
|
36
|
+
redacted_prompt = truncate_prompt(@redactor.redact(prompt))
|
|
37
|
+
|
|
38
|
+
ChaosToTheRescue.logger.debug("Generating fix suggestion for #{context[:exception_class]}")
|
|
39
|
+
|
|
40
|
+
response = call_llm(redacted_prompt)
|
|
41
|
+
parse_fix_suggestion_response(response)
|
|
42
|
+
rescue => e
|
|
43
|
+
ChaosToTheRescue.logger.error("Failed to generate fix suggestion: #{e.message}")
|
|
44
|
+
raise LLMRequestError, "Failed to generate fix suggestion: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def check_llm_availability!
|
|
50
|
+
require "ruby_llm"
|
|
51
|
+
rescue LoadError
|
|
52
|
+
raise LLMNotAvailableError, <<~MSG
|
|
53
|
+
RubyLLM is not available. To use ChaosToTheRescue's LLM features, add this to your Gemfile:
|
|
54
|
+
|
|
55
|
+
gem "ruby_llm", "~> 0.1"
|
|
56
|
+
|
|
57
|
+
Then run: bundle install
|
|
58
|
+
MSG
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def configure_ruby_llm!
|
|
62
|
+
# Configure ruby_llm with API keys from our configuration or ENV
|
|
63
|
+
RubyLLM.configure do |config|
|
|
64
|
+
if ChaosToTheRescue.configuration.openai_api_key
|
|
65
|
+
config.openai_api_key = ChaosToTheRescue.configuration.openai_api_key
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if ChaosToTheRescue.configuration.anthropic_api_key
|
|
69
|
+
config.anthropic_api_key = ChaosToTheRescue.configuration.anthropic_api_key
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
rescue => e
|
|
73
|
+
ChaosToTheRescue.logger.debug("Could not configure ruby_llm: #{e.message}")
|
|
74
|
+
# Continue anyway - ruby_llm will try to use ENV vars
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def call_llm(prompt)
|
|
78
|
+
# Use RubyLLM to interact with the configured model
|
|
79
|
+
model = ChaosToTheRescue.configuration.model
|
|
80
|
+
|
|
81
|
+
chat = RubyLLM.chat(model: model)
|
|
82
|
+
response = chat.ask(prompt)
|
|
83
|
+
|
|
84
|
+
# Return the response content as a string
|
|
85
|
+
response.content
|
|
86
|
+
rescue => e
|
|
87
|
+
raise LLMRequestError, "LLM call failed: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_method_generation_prompt(context)
|
|
91
|
+
<<~PROMPT
|
|
92
|
+
You are a Ruby code generator. Generate a Ruby method implementation based on the following context:
|
|
93
|
+
|
|
94
|
+
Class: #{context[:class_name]}
|
|
95
|
+
Method name: #{context[:method_name]}
|
|
96
|
+
Arguments: #{context[:args].inspect}
|
|
97
|
+
Context: #{context[:additional_context] || "No additional context provided"}
|
|
98
|
+
|
|
99
|
+
#{context[:guidance] ? "IMPORTANT GUIDANCE:\n#{context[:guidance]}\n" : ""}
|
|
100
|
+
|
|
101
|
+
Requirements:
|
|
102
|
+
1. Generate only valid Ruby code
|
|
103
|
+
2. The method should be reasonable and safe
|
|
104
|
+
3. Do not include any dangerous operations (file system access, network calls, etc.)
|
|
105
|
+
4. Return a simple, working implementation
|
|
106
|
+
#{context[:guidance] ? "5. Follow the guidance provided above carefully" : ""}
|
|
107
|
+
|
|
108
|
+
Please respond in the following format:
|
|
109
|
+
METHOD_NAME: <method_name>
|
|
110
|
+
CODE:
|
|
111
|
+
<ruby code here>
|
|
112
|
+
EXPLANATION: <brief explanation of what the method does>
|
|
113
|
+
PROMPT
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_fix_suggestion_prompt(context)
|
|
117
|
+
<<~PROMPT
|
|
118
|
+
You are a Ruby debugging assistant. Analyze the following exception and suggest a fix:
|
|
119
|
+
|
|
120
|
+
Exception class: #{context[:exception_class]}
|
|
121
|
+
Exception message: #{context[:exception_message]}
|
|
122
|
+
Backtrace:
|
|
123
|
+
#{context[:backtrace]&.first(10)&.join("\n") || "No backtrace available"}
|
|
124
|
+
|
|
125
|
+
#{"Additional guidance: #{context[:guidance]}" if context[:guidance]}
|
|
126
|
+
|
|
127
|
+
Controller: #{context[:controller_name] || "N/A"}
|
|
128
|
+
Action: #{context[:action_name] || "N/A"}
|
|
129
|
+
|
|
130
|
+
Please respond in the following format:
|
|
131
|
+
TITLE: <brief title for this fix>
|
|
132
|
+
DIAGNOSIS: <explanation of what went wrong>
|
|
133
|
+
PROPOSED_CHANGES:
|
|
134
|
+
<description of changes to make>
|
|
135
|
+
FILES_TO_EDIT:
|
|
136
|
+
<list of files that should be modified>
|
|
137
|
+
PROMPT
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_method_generation_response(response, method_name)
|
|
141
|
+
# Parse the LLM response into structured data
|
|
142
|
+
lines = response.split("\n")
|
|
143
|
+
|
|
144
|
+
code_start = lines.index { |l| l.strip == "CODE:" }
|
|
145
|
+
explanation_start = lines.index { |l| l.start_with?("EXPLANATION:") }
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
method_name: method_name,
|
|
149
|
+
ruby_code: code_start ? lines[(code_start + 1)...explanation_start].join("\n").strip : "",
|
|
150
|
+
explanation: explanation_start ? lines[explanation_start].sub("EXPLANATION:", "").strip : ""
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_fix_suggestion_response(response)
|
|
155
|
+
# Parse the LLM response into structured data
|
|
156
|
+
lines = response.split("\n")
|
|
157
|
+
|
|
158
|
+
title = lines.find { |l| l.start_with?("TITLE:") }&.sub("TITLE:", "")&.strip || "Suggested Fix"
|
|
159
|
+
diagnosis_idx = lines.index { |l| l.start_with?("DIAGNOSIS:") }
|
|
160
|
+
changes_idx = lines.index { |l| l.start_with?("PROPOSED_CHANGES:") }
|
|
161
|
+
files_idx = lines.index { |l| l.start_with?("FILES_TO_EDIT:") }
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
title: title,
|
|
165
|
+
diagnosis: diagnosis_idx ? lines[diagnosis_idx].sub("DIAGNOSIS:", "").strip : "",
|
|
166
|
+
proposed_changes: (changes_idx && files_idx) ? lines[(changes_idx + 1)...files_idx].join("\n").strip : "",
|
|
167
|
+
files_to_edit: files_idx ? lines[(files_idx + 1)..].map(&:strip).reject(&:empty?) : []
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def truncate_prompt(prompt)
|
|
172
|
+
max_chars = ChaosToTheRescue.configuration.max_prompt_chars
|
|
173
|
+
return prompt if prompt.length <= max_chars
|
|
174
|
+
|
|
175
|
+
truncation_suffix = "\n[TRUNCATED]"
|
|
176
|
+
truncate_at = max_chars - truncation_suffix.length
|
|
177
|
+
|
|
178
|
+
ChaosToTheRescue.logger.warn("Prompt truncated from #{prompt.length} to #{max_chars} characters")
|
|
179
|
+
prompt[0...truncate_at] + truncation_suffix
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module ChaosToTheRescue
|
|
6
|
+
# Logger wrapper that respects configuration settings
|
|
7
|
+
class Logger
|
|
8
|
+
def initialize
|
|
9
|
+
@logger = ::Logger.new($stdout)
|
|
10
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
11
|
+
"[ChaosToTheRescue] #{severity} #{datetime.strftime("%Y-%m-%d %H:%M:%S")}: #{msg}\n"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Log a debug message
|
|
16
|
+
# @param message [String] the message to log
|
|
17
|
+
def debug(message)
|
|
18
|
+
return unless enabled?
|
|
19
|
+
ensure_level
|
|
20
|
+
@logger.debug(message)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Log an info message
|
|
24
|
+
# @param message [String] the message to log
|
|
25
|
+
def info(message)
|
|
26
|
+
return unless enabled?
|
|
27
|
+
ensure_level
|
|
28
|
+
@logger.info(message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Log a warning message
|
|
32
|
+
# @param message [String] the message to log
|
|
33
|
+
def warn(message)
|
|
34
|
+
return unless enabled?
|
|
35
|
+
ensure_level
|
|
36
|
+
@logger.warn(message)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Log an error message
|
|
40
|
+
# @param message [String] the message to log
|
|
41
|
+
def error(message)
|
|
42
|
+
return unless enabled?
|
|
43
|
+
ensure_level
|
|
44
|
+
@logger.error(message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Log a fatal message
|
|
48
|
+
# @param message [String] the message to log
|
|
49
|
+
def fatal(message)
|
|
50
|
+
return unless enabled?
|
|
51
|
+
ensure_level
|
|
52
|
+
@logger.fatal(message)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def enabled?
|
|
58
|
+
ChaosToTheRescue.configuration.enabled
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ensure_level
|
|
62
|
+
@logger.level = log_level_constant
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def log_level_constant
|
|
66
|
+
level = ChaosToTheRescue.configuration.log_level
|
|
67
|
+
case level
|
|
68
|
+
when :debug then ::Logger::DEBUG
|
|
69
|
+
when :info then ::Logger::INFO
|
|
70
|
+
when :warn then ::Logger::WARN
|
|
71
|
+
when :error then ::Logger::ERROR
|
|
72
|
+
when :fatal then ::Logger::FATAL
|
|
73
|
+
else ::Logger::INFO
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Module-level logger instance
|
|
79
|
+
def self.logger
|
|
80
|
+
@logger ||= Logger.new
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChaosToTheRescue
|
|
4
|
+
# Rails integration via Railtie
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
railtie_name :chaos_to_the_rescue
|
|
7
|
+
|
|
8
|
+
# Make RescueFrom available to ActionController
|
|
9
|
+
initializer "chaos_to_the_rescue.action_controller" do
|
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
|
11
|
+
require "chaos_to_the_rescue/rescue_from"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Log when ChaosToTheRescue is loaded in Rails
|
|
16
|
+
config.after_initialize do
|
|
17
|
+
if ChaosToTheRescue.configuration.enabled
|
|
18
|
+
Rails.logger.info("[ChaosToTheRescue] Enabled - method generation and rescue suggestions are active")
|
|
19
|
+
else
|
|
20
|
+
Rails.logger.info("[ChaosToTheRescue] Disabled - use ChaosToTheRescue.configure to enable")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChaosToTheRescue
|
|
4
|
+
# Redacts sensitive information from strings before sending to LLM
|
|
5
|
+
class Redactor
|
|
6
|
+
REDACTED_PLACEHOLDER = "[REDACTED]"
|
|
7
|
+
|
|
8
|
+
# @param patterns [Array<Regexp>] patterns to redact
|
|
9
|
+
def initialize(patterns = nil)
|
|
10
|
+
@patterns = patterns || ChaosToTheRescue.configuration.redact_patterns
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Redact sensitive information from text
|
|
14
|
+
# @param text [String] the text to redact
|
|
15
|
+
# @return [String] the redacted text
|
|
16
|
+
def redact(text)
|
|
17
|
+
return "" if text.nil? || text.empty?
|
|
18
|
+
|
|
19
|
+
redacted = text.dup
|
|
20
|
+
|
|
21
|
+
@patterns.each do |pattern|
|
|
22
|
+
redacted.gsub!(pattern, REDACTED_PLACEHOLDER)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
redacted
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Redact sensitive information from a hash (recursive)
|
|
29
|
+
# @param hash [Hash] the hash to redact
|
|
30
|
+
# @return [Hash] a new hash with redacted values
|
|
31
|
+
def redact_hash(hash)
|
|
32
|
+
return {} unless hash.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
hash.transform_values do |value|
|
|
35
|
+
case value
|
|
36
|
+
when String
|
|
37
|
+
redact(value)
|
|
38
|
+
when Hash
|
|
39
|
+
redact_hash(value)
|
|
40
|
+
when Array
|
|
41
|
+
value.map { |item| item.is_a?(String) ? redact(item) : item }
|
|
42
|
+
else
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Redact sensitive information from an array
|
|
49
|
+
# @param array [Array] the array to redact
|
|
50
|
+
# @return [Array] a new array with redacted values
|
|
51
|
+
def redact_array(array)
|
|
52
|
+
return [] unless array.is_a?(Array)
|
|
53
|
+
|
|
54
|
+
array.map do |item|
|
|
55
|
+
case item
|
|
56
|
+
when String
|
|
57
|
+
redact(item)
|
|
58
|
+
when Hash
|
|
59
|
+
redact_hash(item)
|
|
60
|
+
when Array
|
|
61
|
+
redact_array(item)
|
|
62
|
+
else
|
|
63
|
+
item
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Redact sensitive information from any object by converting to string
|
|
69
|
+
# @param obj [Object] the object to redact
|
|
70
|
+
# @return [String] the redacted string representation
|
|
71
|
+
def redact_object(obj)
|
|
72
|
+
return "" if obj.nil?
|
|
73
|
+
redact(obj.to_s)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module ChaosToTheRescue
|
|
6
|
+
# Controller concern for Rails rescue_from integration
|
|
7
|
+
module RescueFrom
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# Generate a fix suggestion for an exception
|
|
11
|
+
# @param exception [Exception] the exception to analyze
|
|
12
|
+
# @param guidance [String] optional guidance for the LLM
|
|
13
|
+
# @return [Hash] suggestion with :title, :diagnosis, :proposed_changes, :files_to_edit
|
|
14
|
+
def chaos_suggest_fix(exception, guidance: nil)
|
|
15
|
+
unless ChaosToTheRescue.configuration.enabled
|
|
16
|
+
ChaosToTheRescue.logger.warn("chaos_suggest_fix called but ChaosToTheRescue is disabled")
|
|
17
|
+
return {
|
|
18
|
+
title: "ChaosToTheRescue Disabled",
|
|
19
|
+
diagnosis: "ChaosToTheRescue is not enabled. Enable it in your configuration.",
|
|
20
|
+
proposed_changes: "",
|
|
21
|
+
files_to_edit: []
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context = build_exception_context(exception, guidance)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
suggestion = llm_client.suggest_fix(context)
|
|
29
|
+
|
|
30
|
+
# Log the suggestion if configured
|
|
31
|
+
log_suggestion(exception, suggestion) if ChaosToTheRescue.configuration.log_suggestions
|
|
32
|
+
|
|
33
|
+
suggestion
|
|
34
|
+
rescue LLMClient::LLMNotAvailableError => e
|
|
35
|
+
ChaosToTheRescue.logger.error("LLM not available: #{e.message}")
|
|
36
|
+
{
|
|
37
|
+
title: "LLM Not Available",
|
|
38
|
+
diagnosis: e.message,
|
|
39
|
+
proposed_changes: "",
|
|
40
|
+
files_to_edit: []
|
|
41
|
+
}
|
|
42
|
+
rescue => e
|
|
43
|
+
ChaosToTheRescue.logger.error("Failed to generate fix suggestion: #{e.message}")
|
|
44
|
+
{
|
|
45
|
+
title: "Error Generating Suggestion",
|
|
46
|
+
diagnosis: "Failed to generate suggestion: #{e.message}",
|
|
47
|
+
proposed_changes: "",
|
|
48
|
+
files_to_edit: []
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Build context hash for exception analysis
|
|
56
|
+
# @param exception [Exception] the exception
|
|
57
|
+
# @param guidance [String] optional guidance
|
|
58
|
+
# @return [Hash] context for LLM
|
|
59
|
+
def build_exception_context(exception, guidance)
|
|
60
|
+
{
|
|
61
|
+
exception_class: exception.class.name,
|
|
62
|
+
exception_message: sanitize_exception_message(exception.message),
|
|
63
|
+
backtrace: sanitize_backtrace(exception.backtrace),
|
|
64
|
+
guidance: guidance,
|
|
65
|
+
controller_name: controller_name,
|
|
66
|
+
action_name: action_name,
|
|
67
|
+
request_method: request.request_method,
|
|
68
|
+
request_path: request.path,
|
|
69
|
+
params: sanitize_params(params.to_unsafe_h)
|
|
70
|
+
}
|
|
71
|
+
rescue => e
|
|
72
|
+
ChaosToTheRescue.logger.warn("Failed to build full exception context: #{e.message}")
|
|
73
|
+
{
|
|
74
|
+
exception_class: exception.class.name,
|
|
75
|
+
exception_message: exception.message.to_s,
|
|
76
|
+
backtrace: exception.backtrace,
|
|
77
|
+
guidance: guidance
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Sanitize exception message to remove sensitive data
|
|
82
|
+
# @param message [String] the exception message
|
|
83
|
+
# @return [String] sanitized message
|
|
84
|
+
def sanitize_exception_message(message)
|
|
85
|
+
redactor.redact(message.to_s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Sanitize backtrace to remove sensitive paths
|
|
89
|
+
# @param backtrace [Array<String>] the backtrace
|
|
90
|
+
# @return [Array<String>] sanitized backtrace
|
|
91
|
+
def sanitize_backtrace(backtrace)
|
|
92
|
+
return [] unless backtrace
|
|
93
|
+
|
|
94
|
+
backtrace.first(20).map do |line|
|
|
95
|
+
# Replace absolute paths with relative paths for privacy
|
|
96
|
+
line.gsub(Rails.root.to_s, "[RAILS_ROOT]")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sanitize params to remove sensitive data
|
|
101
|
+
# @param params [Hash] the params hash
|
|
102
|
+
# @return [Hash] sanitized params
|
|
103
|
+
def sanitize_params(params)
|
|
104
|
+
# Remove common sensitive keys
|
|
105
|
+
sensitive_keys = %w[password password_confirmation token secret api_key access_token]
|
|
106
|
+
|
|
107
|
+
params.except(*sensitive_keys).transform_values do |value|
|
|
108
|
+
if value.is_a?(String)
|
|
109
|
+
redactor.redact(value)
|
|
110
|
+
else
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Log the suggestion to file
|
|
117
|
+
# @param exception [Exception] the exception
|
|
118
|
+
# @param suggestion [Hash] the suggestion
|
|
119
|
+
def log_suggestion(exception, suggestion)
|
|
120
|
+
log_path = ChaosToTheRescue.configuration.suggestions_log_path
|
|
121
|
+
|
|
122
|
+
# Ensure directory exists
|
|
123
|
+
FileUtils.mkdir_p(File.dirname(log_path))
|
|
124
|
+
|
|
125
|
+
File.open(log_path, "a") do |f|
|
|
126
|
+
f.puts "=" * 80
|
|
127
|
+
f.puts "Timestamp: #{Time.now.iso8601}"
|
|
128
|
+
f.puts "Exception: #{exception.class.name}: #{exception.message}"
|
|
129
|
+
f.puts "Controller: #{controller_name}##{action_name}"
|
|
130
|
+
f.puts "\nSuggestion:"
|
|
131
|
+
f.puts "Title: #{suggestion[:title]}"
|
|
132
|
+
f.puts "Diagnosis: #{suggestion[:diagnosis]}"
|
|
133
|
+
f.puts "\nProposed Changes:"
|
|
134
|
+
f.puts suggestion[:proposed_changes]
|
|
135
|
+
f.puts "\nFiles to Edit:"
|
|
136
|
+
suggestion[:files_to_edit].each { |file| f.puts " - #{file}" }
|
|
137
|
+
f.puts "=" * 80
|
|
138
|
+
f.puts
|
|
139
|
+
end
|
|
140
|
+
rescue => e
|
|
141
|
+
ChaosToTheRescue.logger.error("Failed to log suggestion: #{e.message}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get the LLM client instance
|
|
145
|
+
# @return [LLMClient]
|
|
146
|
+
def llm_client
|
|
147
|
+
@llm_client ||= LLMClient.new
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get the redactor instance
|
|
151
|
+
# @return [Redactor]
|
|
152
|
+
def redactor
|
|
153
|
+
@redactor ||= Redactor.new
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|