Class: RailsConsoleAi::SafetyGuards

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_console_ai/safety_guards.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSafetyGuards

Returns a new instance of SafetyGuards.



29
30
31
32
33
# File 'lib/rails_console_ai/safety_guards.rb', line 29

def initialize
  @guards = {}
  @enabled = true
  @allowlist = {}  # { guard_name => [String or Regexp, ...] }
end

Instance Attribute Details

#guardsObject (readonly)

Returns the value of attribute guards.



27
28
29
# File 'lib/rails_console_ai/safety_guards.rb', line 27

def guards
  @guards
end

Instance Method Details

#add(name, &block) ⇒ Object



35
36
37
# File 'lib/rails_console_ai/safety_guards.rb', line 35

def add(name, &block)
  @guards[name.to_sym] = block
end

#allow(guard_name, key) ⇒ Object

Add a thread-local allowlist entry (runtime “allow for this session”).



71
72
73
74
75
76
# File 'lib/rails_console_ai/safety_guards.rb', line 71

def allow(guard_name, key)
  thread_list = Thread.current[:rails_console_ai_allowlist] ||= {}
  guard_name = guard_name.to_sym
  thread_list[guard_name] ||= []
  thread_list[guard_name] << key unless thread_list[guard_name].include?(key)
end

#allow_global(guard_name, key) ⇒ Object

Add a permanent (config-time) allowlist entry visible to all threads.



64
65
66
67
68
# File 'lib/rails_console_ai/safety_guards.rb', line 64

def allow_global(guard_name, key)
  guard_name = guard_name.to_sym
  @allowlist[guard_name] ||= []
  @allowlist[guard_name] << key unless @allowlist[guard_name].include?(key)
end

#allowed?(guard_name, key) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/rails_console_ai/safety_guards.rb', line 78

def allowed?(guard_name, key)
  guard_name = guard_name.to_sym
  match = ->(entries) {
    entries&.any? do |entry|
      case entry
      when Regexp then key.match?(entry)
      else entry.to_s == key.to_s
      end
    end
  }
  # Check global (config-time) allowlist
  return true if match.call(@allowlist[guard_name])
  # Check thread-local (runtime session) allowlist
  thread_list = Thread.current[:rails_console_ai_allowlist]
  return true if thread_list && match.call(thread_list[guard_name])
  false
end

#allowlistObject



96
97
98
99
100
101
102
# File 'lib/rails_console_ai/safety_guards.rb', line 96

def allowlist
  thread_list = Thread.current[:rails_console_ai_allowlist]
  return @allowlist unless thread_list
  merged = @allowlist.dup
  thread_list.each { |k, v| merged[k] = (merged[k] || []) + v }
  merged
end

#disable!Object



51
52
53
# File 'lib/rails_console_ai/safety_guards.rb', line 51

def disable!
  Thread.current[:rails_console_ai_guards_disabled] = true
end

#empty?Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/rails_console_ai/safety_guards.rb', line 55

def empty?
  @guards.empty?
end

#enable!Object



47
48
49
# File 'lib/rails_console_ai/safety_guards.rb', line 47

def enable!
  Thread.current[:rails_console_ai_guards_disabled] = nil
end

#enabled?Boolean

Returns:

  • (Boolean)


43
44
45
# File 'lib/rails_console_ai/safety_guards.rb', line 43

def enabled?
  @enabled && !Thread.current[:rails_console_ai_guards_disabled]
end

#install_bypass_method!(spec) ⇒ Object

Install a bypass shim for a single method spec (e.g. “ChangeApproval#approve_by!”). Prepends a module that checks the thread-local bypass set at runtime. Idempotent: tracks which specs have been installed to avoid double-prepending.



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
# File 'lib/rails_console_ai/safety_guards.rb', line 131

def install_bypass_method!(spec)
  @installed_bypass_specs ||= Set.new
  return if @installed_bypass_specs.include?(spec)

  if spec.include?('.')
    class_name, method_name = spec.split('.')
    class_method = true
  else
    class_name, method_name = spec.split('#')
    class_method = false
  end

  return unless method_name && !method_name.empty?

  klass = Object.const_get(class_name) rescue return
  method_sym = method_name.to_sym

  bypass_mod = Module.new do
    define_method(method_sym) do |*args, &blk|
      if Thread.current[:rails_console_ai_bypass_methods]&.include?(spec)
        RailsConsoleAi.configuration.safety_guards.without_guards { super(*args, &blk) }
      else
        super(*args, &blk)
      end
    end
  end

  if class_method
    klass.singleton_class.prepend(bypass_mod)
  else
    klass.prepend(bypass_mod)
  end
  @installed_bypass_specs << spec
end

#namesObject



59
60
61
# File 'lib/rails_console_ai/safety_guards.rb', line 59

def names
  @guards.keys
end

#remove(name) ⇒ Object



39
40
41
# File 'lib/rails_console_ai/safety_guards.rb', line 39

def remove(name)
  @guards.delete(name.to_sym)
end

#without_guardsObject

Bypass all safety guards for the duration of the block. Thread-safe: uses a thread-local flag that is restored after the block, even if the block raises an exception.



196
197
198
199
200
201
202
# File 'lib/rails_console_ai/safety_guards.rb', line 196

def without_guards
  prev = Thread.current[:rails_console_ai_bypass_guards]
  Thread.current[:rails_console_ai_bypass_guards] = true
  yield
ensure
  Thread.current[:rails_console_ai_bypass_guards] = prev
end

#wrap(channel_mode: nil, additional_bypass_methods: nil, &block) ⇒ Object

Compose all guards around a block of code. Each guard is an around-block: guard.call { inner } Result: guard_1 { guard_2 { guard_3 { yield } } }



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/rails_console_ai/safety_guards.rb', line 107

def wrap(channel_mode: nil, additional_bypass_methods: nil, &block)
  return yield unless enabled? && !@guards.empty?

  install_skills_once!
  bypass_set = resolve_bypass_methods(channel_mode)
  Array(additional_bypass_methods).each { |m| bypass_set << m }

  prev_active = Thread.current[:rails_console_ai_session_active]
  prev_bypass = Thread.current[:rails_console_ai_bypass_methods]
  Thread.current[:rails_console_ai_session_active] = true
  Thread.current[:rails_console_ai_bypass_methods] = bypass_set
  begin
    @guards.values.reduce(block) { |inner, guard|
      -> { guard.call(&inner) }
    }.call
  ensure
    Thread.current[:rails_console_ai_session_active] = prev_active
    Thread.current[:rails_console_ai_bypass_methods] = prev_bypass
  end
end