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").



87
88
89
90
91
92
# File 'lib/rails_console_ai/safety_guards.rb', line 87

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.



80
81
82
83
84
# File 'lib/rails_console_ai/safety_guards.rb', line 80

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)


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rails_console_ai/safety_guards.rb', line 94

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



112
113
114
115
116
117
118
# File 'lib/rails_console_ai/safety_guards.rb', line 112

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)


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

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.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/rails_console_ai/safety_guards.rb', line 147

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



75
76
77
# File 'lib/rails_console_ai/safety_guards.rb', line 75

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

#with_database_blockedObject

Hard sandbox: block ALL database access (reads and writes) for the duration of the block. Used to fence in sub-agents that must never touch the DB (e.g. the output-explorer, which only examines an in-memory string). Scoped via thread-local, so it covers synchronous work in the calling thread and is restored afterward — nestable, and independent of enable!/disable! and the registered guard set.



60
61
62
63
64
65
66
67
68
69
# File 'lib/rails_console_ai/safety_guards.rb', line 60

def with_database_blocked
  BuiltinGuards.ensure_write_blocker_installed!
  prev = Thread.current[:rails_console_ai_block_all_db]
  Thread.current[:rails_console_ai_block_all_db] = true
  begin
    yield
  ensure
    Thread.current[:rails_console_ai_block_all_db] = prev
  end
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.



212
213
214
215
216
217
218
# File 'lib/rails_console_ai/safety_guards.rb', line 212

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 } } }



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/rails_console_ai/safety_guards.rb', line 123

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