hyperprobe-agent 1.2.27.pre.1

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.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HyperProbe
4
+ module Core
5
+ class TokenBucket
6
+ attr_reader :capacity, :tokens
7
+
8
+ def initialize(capacity, refill_rate_per_sec)
9
+ @capacity = capacity.to_f
10
+ @refill_rate = refill_rate_per_sec.to_f / 1000.0 # tokens per millisecond
11
+ @tokens = @capacity
12
+ @last_refill = monotonic_ms
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def try_consume(count = 1)
17
+ @mutex.synchronize do
18
+ refill
19
+ if @tokens >= count
20
+ @tokens -= count
21
+ true
22
+ else
23
+ false
24
+ end
25
+ end
26
+ end
27
+
28
+ def reserve(count)
29
+ @mutex.synchronize do
30
+ refill
31
+ if @tokens >= count
32
+ @tokens -= count
33
+ BandwidthReservation.new(self, count)
34
+ else
35
+ nil
36
+ end
37
+ end
38
+ end
39
+
40
+ def release_tokens(count)
41
+ @mutex.synchronize do
42
+ @tokens = [@capacity, @tokens + count].min
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def refill
49
+ now = monotonic_ms
50
+ delta = now - @last_refill
51
+ amount = delta * @refill_rate
52
+ @tokens = [@capacity, @tokens + amount].min
53
+ @last_refill = now
54
+ end
55
+
56
+ def monotonic_ms
57
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
58
+ end
59
+ end
60
+
61
+ class BandwidthReservation
62
+ attr_reader :tokens
63
+
64
+ def initialize(bucket, tokens)
65
+ @bucket = bucket
66
+ @tokens = tokens
67
+ @committed = false
68
+ @released = false
69
+ @mutex = Mutex.new
70
+ end
71
+
72
+ def commit
73
+ @mutex.synchronize do
74
+ @committed = true
75
+ end
76
+ end
77
+
78
+ def release
79
+ @mutex.synchronize do
80
+ return if @committed || @released
81
+
82
+ @released = true
83
+ @bucket.release_tokens(@tokens)
84
+ end
85
+ end
86
+ end
87
+
88
+ class QuotaManager
89
+ def initialize(hits_per_sec = 10, bytes_per_sec = 200 * 1024)
90
+ @eval_bucket = TokenBucket.new(hits_per_sec, hits_per_sec)
91
+ @bandwidth_bucket = TokenBucket.new(bytes_per_sec, bytes_per_sec)
92
+ end
93
+
94
+ def can_evaluate?
95
+ @eval_bucket.try_consume(1)
96
+ end
97
+
98
+ alias can_evaluate can_evaluate?
99
+
100
+ def can_send?(bytes)
101
+ @bandwidth_bucket.try_consume(bytes)
102
+ end
103
+
104
+ alias can_send can_send?
105
+
106
+ def reserve_bandwidth(bytes)
107
+ @bandwidth_bucket.reserve(bytes)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
4
+
5
+ module HyperProbe
6
+ module Core
7
+ class SafeASTValidator
8
+ MAX_EXPRESSION_LENGTH = 256
9
+ CACHE_CAPACITY = 200
10
+
11
+ FORBIDDEN_OPERATORS = %w[
12
+ = += -= *= /= %= << >> |= &= ^=
13
+ ].freeze
14
+
15
+ FORBIDDEN_IDENTIFIERS = %w[
16
+ system exec spawn fork exit exit! abort
17
+ eval instance_eval class_eval module_eval
18
+ send __send__ public_send define_method
19
+ const_set const_get instance_variable_set remove_instance_variable
20
+ delete delete! delete_all destroy destroy!
21
+ update update! update_all save save! create create!
22
+ truncate drop clear write require load
23
+ ].freeze
24
+
25
+ FORBIDDEN_CONSTANTS = %w[
26
+ File Dir IO FileUtils Socket TCPSocket UDPSocket Net
27
+ Process Kernel ObjectSpace GC System
28
+ ].freeze
29
+
30
+ @cache = {}
31
+ @cache_mutex = Mutex.new
32
+
33
+ class << self
34
+ def validate!(expression)
35
+ return if expression.nil? || expression.strip.empty?
36
+
37
+ clean_expr = expression.strip
38
+ if clean_expr.length > MAX_EXPRESSION_LENGTH
39
+ raise SecurityError, "Expression is too long (max #{MAX_EXPRESSION_LENGTH} characters)."
40
+ end
41
+
42
+ @cache_mutex.synchronize do
43
+ return if @cache[clean_expr]
44
+ end
45
+
46
+ tokens = Ripper.lex(clean_expr)
47
+
48
+ tokens.each_with_index do |tok, idx|
49
+ _pos, type, text, _state = tok
50
+
51
+ # 1. Reject Shell Backtick / Command Execution (`...` or %x{...})
52
+ if type == :on_backtick || type == :on_xstring_beg
53
+ raise SecurityError, 'Shell command execution is strictly forbidden in probe expressions.'
54
+ end
55
+
56
+ # 2. Reject Mutating Operators and Assignments (=, +=, -=, <<, etc.)
57
+ if type == :on_op && FORBIDDEN_OPERATORS.include?(text)
58
+ raise SecurityError, "Assignments and state mutations ('#{text}') are strictly forbidden in probe expressions."
59
+ end
60
+
61
+ # 3. Reject Forbidden System, File, and Metaprogramming Identifiers
62
+ if (type == :on_ident || type == :on_kw) && FORBIDDEN_IDENTIFIERS.include?(text)
63
+ raise SecurityError, "Method or function invocation '#{text}()' is strictly forbidden during probe evaluation."
64
+ end
65
+
66
+ # 4. Reject Dangerous Class Invocations (File.delete, Net::HTTP, etc.)
67
+ if type == :on_const && FORBIDDEN_CONSTANTS.include?(text)
68
+ # Check if followed by a method call like File.read or Net::HTTP.get
69
+ next_tokens = tokens[(idx + 1)..]
70
+ has_method_call = next_tokens&.any? { |t| t[1] == :on_period || t[1] == :on_op && t[2] == '::' }
71
+ if has_method_call
72
+ raise SecurityError, "Access to system constant '#{text}' is strictly forbidden in probe expressions."
73
+ end
74
+ end
75
+ end
76
+
77
+ @cache_mutex.synchronize do
78
+ @cache.shift if @cache.size >= CACHE_CAPACITY
79
+ @cache[clean_expr] = true
80
+ end
81
+ end
82
+
83
+ def reset_cache!
84
+ @cache_mutex.synchronize do
85
+ @cache.clear
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HyperProbe
4
+ module Core
5
+ module AgentHealth
6
+ GREEN = :GREEN
7
+ YELLOW = :YELLOW
8
+ RED = :RED
9
+ end
10
+
11
+ class SafetyMonitor
12
+ SAMPLE_INTERVAL_SECONDS = 0.1
13
+ LAG_WINDOW_SIZE = 10
14
+ LAG_YELLOW_BREACHES = 4
15
+ LAG_RED_BREACHES = 7
16
+ SEVERE_LAG_WINDOW_SIZE = 5
17
+ SEVERE_LAG_RED_BREACHES = 3
18
+ SEVERE_LAG_MULTIPLIER = 4.0
19
+
20
+ attr_reader :health, :last_thread_lag_ms, :cumulative_pause_time, :max_lag_ms, :pause_budget_ms
21
+
22
+ def initialize(on_state_change, max_lag_ms: 50.0, pause_budget_ms: 15.0, is_ephemeral_lambda: false)
23
+ @on_state_change = on_state_change
24
+ @max_lag_ms = max_lag_ms.to_f
25
+ @pause_budget_ms = pause_budget_ms.to_f
26
+ @is_ephemeral_lambda = is_ephemeral_lambda
27
+
28
+ @health = AgentHealth::GREEN
29
+ @last_heartbeat = monotonic_ms
30
+ @last_thread_lag_ms = 0.0
31
+ @thread_lag_samples = []
32
+ @cumulative_pause_time = 0.0
33
+ @last_window_reset = monotonic_ms
34
+
35
+ @mutex = Mutex.new
36
+ @notification_mutex = Mutex.new
37
+ @running = false
38
+ @thread = nil
39
+ end
40
+
41
+ def start
42
+ @mutex.synchronize do
43
+ return if @running
44
+
45
+ @running = true
46
+ @last_heartbeat = monotonic_ms
47
+ @last_window_reset = monotonic_ms
48
+
49
+ @thread = Thread.new { monitor_loop }
50
+ @thread.name = 'hyperprobe-safety' if @thread.respond_to?(:name=)
51
+ end
52
+ end
53
+
54
+ def stop
55
+ @mutex.synchronize do
56
+ @running = false
57
+ end
58
+ @thread&.wakeup if @thread&.alive?
59
+ @thread&.join(1.0) rescue nil
60
+ @thread = nil
61
+ end
62
+
63
+ def report_pause_duration(ms)
64
+ callback_info = nil
65
+ @mutex.synchronize do
66
+ @cumulative_pause_time += ms.to_f
67
+ callback_info = evaluate_health_locked
68
+ end
69
+ trigger_callback(callback_info) if callback_info
70
+ end
71
+
72
+ def record_thread_lag(thread_lag_ms)
73
+ callback_info = nil
74
+ @mutex.synchronize do
75
+ record_thread_lag_locked(thread_lag_ms)
76
+ callback_info = evaluate_health_locked
77
+ end
78
+ trigger_callback(callback_info) if callback_info
79
+ end
80
+
81
+ def get_health
82
+ @mutex.synchronize { @health }
83
+ end
84
+
85
+ private
86
+
87
+ def monitor_loop
88
+ while @running
89
+ wait_started = monotonic_ms
90
+ sleep SAMPLE_INTERVAL_SECONDS
91
+ break unless @running
92
+
93
+ callback_info = nil
94
+ @mutex.synchronize do
95
+ now = monotonic_ms
96
+ # Expected interval is 100ms; excess time is lag/jitter
97
+ thread_lag_ms = [0.0, now - wait_started - (SAMPLE_INTERVAL_SECONDS * 1000.0)].max
98
+ record_thread_lag_locked(thread_lag_ms)
99
+
100
+ # Reset pause budget window every second
101
+ if now - @last_window_reset > 1000.0
102
+ @cumulative_pause_time = 0.0
103
+ @last_window_reset = now
104
+ end
105
+
106
+ callback_info = evaluate_health_locked
107
+ end
108
+
109
+ trigger_callback(callback_info) if callback_info
110
+ end
111
+ end
112
+
113
+ def record_thread_lag_locked(lag_ms)
114
+ clean_lag = [0.0, lag_ms.to_f].max
115
+ @last_thread_lag_ms = clean_lag
116
+ @thread_lag_samples << clean_lag
117
+ @thread_lag_samples.shift while @thread_lag_samples.length > LAG_WINDOW_SIZE
118
+ end
119
+
120
+ def evaluate_health_locked
121
+ prev_health = @health
122
+ reason = nil
123
+
124
+ if @is_ephemeral_lambda
125
+ @health = AgentHealth::GREEN
126
+ return nil if prev_health == AgentHealth::GREEN
127
+
128
+ return [@health, 'System stabilized in Lambda mode.']
129
+ end
130
+
131
+ recent_lags = @thread_lag_samples.dup
132
+ lag_breaches = recent_lags.count { |lag| lag > @max_lag_ms }
133
+ severe_lag_ms = @max_lag_ms * SEVERE_LAG_MULTIPLIER
134
+ severe_window = recent_lags.last(SEVERE_LAG_WINDOW_SIZE)
135
+ severe_lag_breaches = severe_window.count { |lag| lag > severe_lag_ms }
136
+
137
+ if severe_lag_breaches >= SEVERE_LAG_RED_BREACHES
138
+ @health = AgentHealth::RED
139
+ reason = "Severe Execution Thread Lag exceeded #{severe_lag_ms.round(1)}ms in #{severe_lag_breaches}/#{SEVERE_LAG_WINDOW_SIZE} recent readings"
140
+ elsif lag_breaches >= LAG_RED_BREACHES
141
+ @health = AgentHealth::RED
142
+ reason = "Execution Thread Lag exceeded #{@max_lag_ms.round(1)}ms in #{lag_breaches}/#{LAG_WINDOW_SIZE} recent readings"
143
+ elsif @cumulative_pause_time > @pause_budget_ms
144
+ @health = AgentHealth::RED
145
+ reason = "Cumulative Pause Budget (#{@cumulative_pause_time.round(1)}ms) exceeded limit (#{@pause_budget_ms.round(1)}ms)"
146
+ elsif severe_lag_breaches.positive?
147
+ @health = AgentHealth::YELLOW
148
+ reason = "Moderate impact: Severe Execution Thread Lag exceeded #{severe_lag_ms.round(1)}ms in #{severe_lag_breaches}/#{SEVERE_LAG_WINDOW_SIZE} recent readings"
149
+ elsif lag_breaches >= LAG_YELLOW_BREACHES
150
+ @health = AgentHealth::YELLOW
151
+ reason = "Moderate impact: Execution Thread Lag exceeded #{@max_lag_ms.round(1)}ms in #{lag_breaches}/#{LAG_WINDOW_SIZE} recent readings"
152
+ elsif @cumulative_pause_time > (@pause_budget_ms / 2.0)
153
+ @health = AgentHealth::YELLOW
154
+ reason = "Moderate impact: Cumulative Pause Budget (#{@cumulative_pause_time.round(1)}ms) reached 50% of limit (#{@pause_budget_ms.round(1)}ms)"
155
+ else
156
+ @health = AgentHealth::GREEN
157
+ reason = 'System stabilized.' if prev_health != AgentHealth::GREEN
158
+ end
159
+
160
+ return unless @health != prev_health
161
+
162
+ [@health, reason]
163
+ end
164
+
165
+ def trigger_callback(callback_info)
166
+ health, reason = callback_info
167
+ @notification_mutex.synchronize do
168
+ @on_state_change&.call(health, reason)
169
+ end
170
+ rescue StandardError
171
+ # Never crash application thread or safety monitor on callback error
172
+ end
173
+
174
+ def monotonic_ms
175
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
176
+ end
177
+ end
178
+ end
179
+ end