rails_console_ai 0.33.0 → 0.34.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +5 -3
- data/app/helpers/rails_console_ai/sessions_helper.rb +1 -1
- data/app/views/layouts/rails_console_ai/application.html.erb +5 -0
- data/app/views/rails_console_ai/sessions/index.html.erb +2 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +7 -0
- data/lib/rails_console_ai/configuration.rb +7 -4
- data/lib/rails_console_ai/conversation_engine.rb +22 -2
- data/lib/rails_console_ai/safety_guards.rb +75 -0
- data/lib/rails_console_ai/session_logger.rb +13 -1
- data/lib/rails_console_ai/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61fcabfed3f03e58e104b7c8b49b86ccf365db5dbae5e1dbf1f8f1b7e63d811c
|
|
4
|
+
data.tar.gz: ec844ca4e1251fb9c1f0c0172095050745ef9690a90defefbcded92eae0962f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2294a51714714de69afcc908dccbcb8d5034999973c40b4533600579e8617efc1133ed918ed7925aa082a3afd5598335b5a09527d1a933b38a6ddbf8143d595
|
|
7
|
+
data.tar.gz: bce68674c9467a92eac6b7521e4f1e39f1d06c32b374f38f627513c7ab31bdda57a12a2e937654103a2322d075b594aa56b610cf2ad716f1965f1c2d6123cfad
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.34.0]
|
|
6
|
+
|
|
7
|
+
- Add an `:in_process_requests` built-in safety guard that blocks in-process HTTP dispatch against the app itself — `ActionDispatch::Integration::Session` requests (the console `app` helper) and direct Rack dispatch (`Rails.application.call`) — for all verbs including GET, since these can hang the session thread indefinitely; allowlist entries are request paths
|
|
8
|
+
- Create/update the session record with a `running` status before each turn's tool loop starts, so a turn that hangs or dies mid-loop still leaves a visible session record, and show each session's status in the admin sessions page
|
|
9
|
+
- Make session logging drop attributes the sessions table doesn't have a column for yet, so a gem newer than the table degrades to a partial row instead of losing the insert
|
|
10
|
+
- Fix the sessions page cost display after the pricing refactor
|
|
11
|
+
|
|
5
12
|
## [0.33.0]
|
|
6
13
|
|
|
7
14
|
- Support Claude Opus 5 and make it the default model for the Anthropic and Bedrock providers
|
data/README.md
CHANGED
|
@@ -176,15 +176,17 @@ Safety guards prevent AI-generated code from causing side effects. When a guard
|
|
|
176
176
|
|
|
177
177
|
```ruby
|
|
178
178
|
RailsConsoleAi.configure do |config|
|
|
179
|
-
config.use_builtin_safety_guard :database_writes
|
|
180
|
-
config.use_builtin_safety_guard :http_mutations
|
|
181
|
-
config.use_builtin_safety_guard :mailers
|
|
179
|
+
config.use_builtin_safety_guard :database_writes # blocks INSERT/UPDATE/DELETE/DROP/etc.
|
|
180
|
+
config.use_builtin_safety_guard :http_mutations # blocks POST/PUT/PATCH/DELETE via Net::HTTP
|
|
181
|
+
config.use_builtin_safety_guard :mailers # disables ActionMailer delivery
|
|
182
|
+
config.use_builtin_safety_guard :in_process_requests # blocks in-process requests against the app itself
|
|
182
183
|
end
|
|
183
184
|
```
|
|
184
185
|
|
|
185
186
|
- **`:database_writes`** — intercepts the ActiveRecord connection adapter to block write SQL. Works on Rails 5+ with any database adapter.
|
|
186
187
|
- **`:http_mutations`** — intercepts `Net::HTTP#request` to block non-GET/HEAD/OPTIONS requests. Covers libraries built on Net::HTTP (HTTParty, RestClient, Faraday).
|
|
187
188
|
- **`:mailers`** — sets `ActionMailer::Base.perform_deliveries = false` during execution.
|
|
189
|
+
- **`:in_process_requests`** — blocks `ActionDispatch::Integration::Session` requests (the console `app` helper) and direct Rack dispatch (`Rails.application.call`). These run the app's full middleware stack inside the current process and can deadlock or hang the session thread indefinitely, so **all verbs are blocked, including GET**. Allowlist entries are request paths.
|
|
188
190
|
|
|
189
191
|
### Custom Guards
|
|
190
192
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module RailsConsoleAi
|
|
2
2
|
module SessionsHelper
|
|
3
3
|
def estimated_cost(session)
|
|
4
|
-
pricing = Configuration
|
|
4
|
+
pricing = Configuration.pricing_for(session.model)
|
|
5
5
|
return nil unless pricing
|
|
6
6
|
|
|
7
7
|
(session.input_tokens * pricing[:input]) + (session.output_tokens * pricing[:output])
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
.badge-one_shot { background: #d4edda; color: #155724; }
|
|
36
36
|
.badge-interactive { background: #cce5ff; color: #004085; }
|
|
37
37
|
.badge-explain { background: #fff3cd; color: #856404; }
|
|
38
|
+
.badge-status-running { background: #fff3cd; color: #856404; }
|
|
39
|
+
.badge-status-queued { background: #e2e3e5; color: #383d41; }
|
|
40
|
+
.badge-status-ready { background: #d4edda; color: #155724; }
|
|
41
|
+
.badge-status-failed { background: #f8d7da; color: #721c24; }
|
|
42
|
+
.badge-status-aborted { background: #f8d7da; color: #721c24; }
|
|
38
43
|
.meta-card {
|
|
39
44
|
background: #fff; border-radius: 8px; padding: 20px;
|
|
40
45
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<th>Name</th>
|
|
18
18
|
<th style="max-width: 400px;">Query</th>
|
|
19
19
|
<th>Mode</th>
|
|
20
|
+
<th>Status</th>
|
|
20
21
|
<th>Tokens</th>
|
|
21
22
|
<th>Cost</th>
|
|
22
23
|
<th>Duration</th>
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
<td><%= session.name.present? ? session.name : '-' %></td>
|
|
32
33
|
<td class="query-cell"><a href="<%= rails_console_ai.session_path(session) %>" title="<%= h session.query.truncate(200) %>"><%= truncate(session.query.gsub(/\s+/, ' ').strip, length: 80) %></a></td>
|
|
33
34
|
<td><span class="badge badge-<%= session.mode %>"><%= session.mode %></span></td>
|
|
35
|
+
<td><% status = session.try(:status) %><%= status.present? ? content_tag(:span, status, class: "badge badge-status-#{status}") : '-' %></td>
|
|
34
36
|
<td class="mono"><%= session.input_tokens + session.output_tokens %></td>
|
|
35
37
|
<td class="mono"><%= format_cost(session) %></td>
|
|
36
38
|
<td class="mono"><%= session.duration_ms ? "#{session.duration_ms}ms" : '-' %></td>
|
|
@@ -72,6 +72,13 @@ RailsConsoleAi.configure do |config|
|
|
|
72
72
|
# Built-in guard for mailers — disables ActionMailer delivery:
|
|
73
73
|
# config.use_builtin_safety_guard :mailers
|
|
74
74
|
#
|
|
75
|
+
# Built-in guard for in-process requests — blocks ActionDispatch::Integration::Session
|
|
76
|
+
# (the console `app` helper) and direct Rack dispatch against the running app.
|
|
77
|
+
# These run the full middleware stack inside this process and can hang the session
|
|
78
|
+
# thread indefinitely, so ALL verbs are blocked (including GET). Strongly recommended
|
|
79
|
+
# for Slack/API channels:
|
|
80
|
+
# config.use_builtin_safety_guard :in_process_requests
|
|
81
|
+
#
|
|
75
82
|
# config.safety_guard :jobs do |&execute|
|
|
76
83
|
# Sidekiq::Testing.fake! { execute.call }
|
|
77
84
|
# end
|
|
@@ -172,12 +172,13 @@ module RailsConsoleAi
|
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
# Register a built-in safety guard by name.
|
|
175
|
-
# Available: :database_writes, :http_mutations, :mailers
|
|
175
|
+
# Available: :database_writes, :http_mutations, :mailers, :in_process_requests
|
|
176
176
|
#
|
|
177
177
|
# Options:
|
|
178
178
|
# allow: Array of strings or regexps to allowlist for this guard.
|
|
179
|
-
# - :http_mutations
|
|
180
|
-
# - :database_writes
|
|
179
|
+
# - :http_mutations → hosts (e.g. "s3.amazonaws.com", /googleapis\.com/)
|
|
180
|
+
# - :database_writes → table names (e.g. "rails_console_ai_sessions")
|
|
181
|
+
# - :in_process_requests → request paths (e.g. "/health")
|
|
181
182
|
def use_builtin_safety_guard(name, allow: nil)
|
|
182
183
|
require 'rails_console_ai/safety_guards'
|
|
183
184
|
guard_name = name.to_sym
|
|
@@ -188,8 +189,10 @@ module RailsConsoleAi
|
|
|
188
189
|
safety_guards.add(:http_mutations, &BuiltinGuards.http_mutations)
|
|
189
190
|
when :mailers
|
|
190
191
|
safety_guards.add(:mailers, &BuiltinGuards.mailers)
|
|
192
|
+
when :in_process_requests
|
|
193
|
+
safety_guards.add(:in_process_requests, &BuiltinGuards.in_process_requests)
|
|
191
194
|
else
|
|
192
|
-
raise ConfigurationError, "Unknown built-in safety guard: #{name}. Available: database_writes, http_mutations, mailers"
|
|
195
|
+
raise ConfigurationError, "Unknown built-in safety guard: #{name}. Available: database_writes, http_mutations, mailers, in_process_requests"
|
|
193
196
|
end
|
|
194
197
|
|
|
195
198
|
if allow
|
|
@@ -242,6 +242,8 @@ module RailsConsoleAi
|
|
|
242
242
|
end
|
|
243
243
|
|
|
244
244
|
def execute_direct(raw_code)
|
|
245
|
+
@interactive_query ||= "> #{raw_code}"
|
|
246
|
+
log_interactive_turn(status: 'running')
|
|
245
247
|
exec_result = @executor.execute_unsafe(raw_code)
|
|
246
248
|
|
|
247
249
|
output_parts = []
|
|
@@ -260,14 +262,31 @@ module RailsConsoleAi
|
|
|
260
262
|
end
|
|
261
263
|
@history << { role: :user, content: context_msg, output_id: output_id }
|
|
262
264
|
|
|
263
|
-
@interactive_query ||= "> #{raw_code}"
|
|
264
265
|
@last_interactive_code = raw_code
|
|
265
266
|
@last_interactive_output = @executor.last_output
|
|
266
267
|
@last_interactive_result = exec_result ? exec_result.inspect : nil
|
|
267
268
|
@last_interactive_executed = true
|
|
269
|
+
log_interactive_turn(status: 'ready')
|
|
268
270
|
end
|
|
269
271
|
|
|
272
|
+
# Wraps run_turn with session-row status tracking. The row is created/updated
|
|
273
|
+
# with status 'running' BEFORE the tool loop starts, so a turn that hangs or
|
|
274
|
+
# dies mid-loop (hung eval, OOM-killed pod, deploy) still leaves a visible
|
|
275
|
+
# session record instead of vanishing without a trace.
|
|
270
276
|
def send_and_execute
|
|
277
|
+
log_interactive_turn(status: 'running')
|
|
278
|
+
status = run_turn
|
|
279
|
+
log_interactive_turn(status: status == :error ? 'failed' : 'ready')
|
|
280
|
+
status
|
|
281
|
+
rescue Interrupt
|
|
282
|
+
log_interactive_turn(status: 'ready')
|
|
283
|
+
raise
|
|
284
|
+
rescue StandardError
|
|
285
|
+
log_interactive_turn(status: 'failed')
|
|
286
|
+
raise
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def run_turn
|
|
271
290
|
begin
|
|
272
291
|
result, tool_messages, last_llm_stats = send_query(nil, conversation: @history)
|
|
273
292
|
rescue Providers::ProviderError => e
|
|
@@ -568,7 +587,7 @@ module RailsConsoleAi
|
|
|
568
587
|
|
|
569
588
|
# --- Session logging ---
|
|
570
589
|
|
|
571
|
-
def log_interactive_turn
|
|
590
|
+
def log_interactive_turn(status: nil)
|
|
572
591
|
require 'rails_console_ai/session_logger'
|
|
573
592
|
session_attrs = {
|
|
574
593
|
conversation: @history,
|
|
@@ -580,6 +599,7 @@ module RailsConsoleAi
|
|
|
580
599
|
executed: @last_interactive_executed,
|
|
581
600
|
console_output: @channel.respond_to?(:console_capture_string) ? @channel.console_capture_string : nil
|
|
582
601
|
}
|
|
602
|
+
session_attrs[:status] = status if status
|
|
583
603
|
|
|
584
604
|
if @interactive_session_id
|
|
585
605
|
SessionLogger.update(@interactive_session_id, session_attrs)
|
|
@@ -364,6 +364,81 @@ module RailsConsoleAi
|
|
|
364
364
|
}
|
|
365
365
|
end
|
|
366
366
|
|
|
367
|
+
# Blocks in-process HTTP dispatch against the running app itself.
|
|
368
|
+
# An ActionDispatch::Integration::Session request (the console `app` helper,
|
|
369
|
+
# or a manually built integration session) runs the app's full middleware
|
|
370
|
+
# stack inside the current process and can deadlock or hang the session
|
|
371
|
+
# thread indefinitely — so ALL verbs are blocked, including GET.
|
|
372
|
+
module InProcessRequestBlocker
|
|
373
|
+
def process(*args, **kwargs, &block)
|
|
374
|
+
RailsConsoleAi::BuiltinGuards.check_in_process_request!(args[0], args[1])
|
|
375
|
+
super
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Backstop for the same hazard via direct Rack dispatch
|
|
380
|
+
# (e.g. Rails.application.call(env)), which bypasses Integration::Session.
|
|
381
|
+
module EngineCallBlocker
|
|
382
|
+
def call(env, *args)
|
|
383
|
+
if env.is_a?(Hash)
|
|
384
|
+
RailsConsoleAi::BuiltinGuards.check_in_process_request!(env['REQUEST_METHOD'], env['PATH_INFO'])
|
|
385
|
+
end
|
|
386
|
+
super
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def self.check_in_process_request!(http_method, path)
|
|
391
|
+
return unless Thread.current[:rails_console_ai_block_in_process_requests]
|
|
392
|
+
return if Thread.current[:rails_console_ai_bypass_guards]
|
|
393
|
+
|
|
394
|
+
key = path.to_s
|
|
395
|
+
guards = RailsConsoleAi.configuration.safety_guards
|
|
396
|
+
return if !key.empty? && guards.allowed?(:in_process_requests, key)
|
|
397
|
+
|
|
398
|
+
label = [http_method.to_s.upcase, key].reject(&:empty?).join(' ')
|
|
399
|
+
raise RailsConsoleAi::SafetyError.new(
|
|
400
|
+
"In-process HTTP request blocked (#{label.empty? ? 'app dispatch' : label}). " \
|
|
401
|
+
"Dispatching a request through the app's own middleware stack from this session " \
|
|
402
|
+
"can hang the process indefinitely, even for GET. Do not retry via another route " \
|
|
403
|
+
"or Rack — call the controller's underlying service or model code directly instead.",
|
|
404
|
+
guard: :in_process_requests,
|
|
405
|
+
blocked_key: key.empty? ? nil : key
|
|
406
|
+
)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def self.in_process_requests
|
|
410
|
+
->(&block) {
|
|
411
|
+
ensure_in_process_blocker_installed!
|
|
412
|
+
prev = Thread.current[:rails_console_ai_block_in_process_requests]
|
|
413
|
+
Thread.current[:rails_console_ai_block_in_process_requests] = true
|
|
414
|
+
begin
|
|
415
|
+
block.call
|
|
416
|
+
ensure
|
|
417
|
+
Thread.current[:rails_console_ai_block_in_process_requests] = prev
|
|
418
|
+
end
|
|
419
|
+
}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def self.ensure_in_process_blocker_installed!
|
|
423
|
+
return if @in_process_blocker_installed
|
|
424
|
+
|
|
425
|
+
begin
|
|
426
|
+
require 'action_dispatch'
|
|
427
|
+
require 'action_dispatch/testing/integration'
|
|
428
|
+
rescue LoadError, NameError
|
|
429
|
+
nil # actionpack not (fully) available — the Engine backstop may still apply
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
if defined?(ActionDispatch::Integration::Session) &&
|
|
433
|
+
!ActionDispatch::Integration::Session.ancestors.include?(InProcessRequestBlocker)
|
|
434
|
+
ActionDispatch::Integration::Session.prepend(InProcessRequestBlocker)
|
|
435
|
+
end
|
|
436
|
+
if defined?(Rails::Engine) && !Rails::Engine.ancestors.include?(EngineCallBlocker)
|
|
437
|
+
Rails::Engine.prepend(EngineCallBlocker)
|
|
438
|
+
end
|
|
439
|
+
@in_process_blocker_installed = true
|
|
440
|
+
end
|
|
441
|
+
|
|
367
442
|
def self.ensure_http_blocker_installed!
|
|
368
443
|
return if @http_blocker_installed
|
|
369
444
|
|
|
@@ -32,7 +32,7 @@ module RailsConsoleAi
|
|
|
32
32
|
opts = attrs[:options]
|
|
33
33
|
create_attrs[:options] = opts.is_a?(String) ? opts : opts.to_json
|
|
34
34
|
end
|
|
35
|
-
record = session_class.create!(create_attrs)
|
|
35
|
+
record = session_class.create!(filter_to_columns(create_attrs))
|
|
36
36
|
record.id
|
|
37
37
|
rescue => e
|
|
38
38
|
msg = "RailsConsoleAi: session logging failed: #{e.class}: #{e.message}"
|
|
@@ -70,6 +70,7 @@ module RailsConsoleAi
|
|
|
70
70
|
updates[:result] = attrs[:result] if attrs.key?(:result)
|
|
71
71
|
updates[:error_message] = attrs[:error_message] if attrs.key?(:error_message)
|
|
72
72
|
|
|
73
|
+
updates = filter_to_columns(updates)
|
|
73
74
|
session_class.where(id: id).update_all(updates) unless updates.empty?
|
|
74
75
|
rescue => e
|
|
75
76
|
msg = "RailsConsoleAi: session update failed: #{e.class}: #{e.message}"
|
|
@@ -80,6 +81,17 @@ module RailsConsoleAi
|
|
|
80
81
|
|
|
81
82
|
private
|
|
82
83
|
|
|
84
|
+
# Drop attrs the table doesn't have a column for, so a gem that's newer
|
|
85
|
+
# than the table (e.g. status added before RailsConsoleAi.migrate! ran)
|
|
86
|
+
# degrades to a partial row instead of losing the whole insert.
|
|
87
|
+
def filter_to_columns(attrs)
|
|
88
|
+
return attrs unless session_class.respond_to?(:column_names)
|
|
89
|
+
cols = session_class.column_names.map(&:to_s)
|
|
90
|
+
attrs.select { |k, _| cols.include?(k.to_s) }
|
|
91
|
+
rescue StandardError
|
|
92
|
+
attrs
|
|
93
|
+
end
|
|
94
|
+
|
|
83
95
|
def table_exists?
|
|
84
96
|
# Only cache positive results — retry on failure so transient
|
|
85
97
|
# errors (boot timing, connection not ready) don't stick forever
|