Module: Legion::MCP::ContextGuard

Extended by:
Logging::Helper
Defined in:
lib/legion/mcp/context_guard.rb

Constant Summary collapse

DEFAULT_MAX_STALE_SECONDS =
3600
DEFAULT_RAPID_FIRE_THRESHOLD =
5
DEFAULT_RAPID_FIRE_WINDOW_SECS =
600
DEFAULT_ANOMALY_MISS_THRESHOLD =
2

Class Method Summary collapse

Class Method Details

.anomalous?(pattern) ⇒ Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/legion/mcp/context_guard.rb', line 42

def anomalous?(pattern)
  (pattern[:miss_count] || 0) >= anomaly_miss_threshold
end

.anomaly_failure(pattern) ⇒ Object



89
90
91
# File 'lib/legion/mcp/context_guard.rb', line 89

def anomaly_failure(pattern)
  { passed: false, guard: :anomaly, reason: "#{pattern[:miss_count]} consecutive misses" }
end

.anomaly_miss_thresholdObject



73
74
75
# File 'lib/legion/mcp/context_guard.rb', line 73

def anomaly_miss_threshold
  DEFAULT_ANOMALY_MISS_THRESHOLD
end

.check(pattern, _params, _context) ⇒ Object



15
16
17
18
19
20
21
22
# File 'lib/legion/mcp/context_guard.rb', line 15

def check(pattern, _params, _context)
  log.debug("[mcp][guard] action=check intent_hash=#{pattern[:intent_hash]&.[](0, 12)}")
  return staleness_failure(pattern) if stale?(pattern)
  return anomaly_failure(pattern) if anomalous?(pattern)
  return rapid_fire_failure(pattern) if rapid_fire?(pattern[:intent_hash])

  { passed: true }
end

.max_stale_secondsObject



61
62
63
# File 'lib/legion/mcp/context_guard.rb', line 61

def max_stale_seconds
  setting(:max_stale_seconds) || DEFAULT_MAX_STALE_SECONDS
end

.mutexObject



102
103
104
# File 'lib/legion/mcp/context_guard.rb', line 102

def mutex
  @mutex ||= Mutex.new
end

.rapid_fire?(intent_hash) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/legion/mcp/context_guard.rb', line 46

def rapid_fire?(intent_hash)
  return false unless intent_hash

  window = Time.now - rapid_fire_window_seconds
  count = mutex.synchronize do
    entries = requests[intent_hash]
    return false unless entries

    entries.reject! { |t| t < window }
    entries.size
  end

  count > rapid_fire_threshold
end

.rapid_fire_failure(_pattern) ⇒ Object



93
94
95
96
# File 'lib/legion/mcp/context_guard.rb', line 93

def rapid_fire_failure(_pattern)
  { passed: false, guard: :rapid_fire,
    reason: "exceeded #{rapid_fire_threshold} requests in #{rapid_fire_window_seconds}s" }
end

.rapid_fire_thresholdObject



65
66
67
# File 'lib/legion/mcp/context_guard.rb', line 65

def rapid_fire_threshold
  setting(:rapid_fire_threshold) || DEFAULT_RAPID_FIRE_THRESHOLD
end

.rapid_fire_window_secondsObject



69
70
71
# File 'lib/legion/mcp/context_guard.rb', line 69

def rapid_fire_window_seconds
  setting(:rapid_fire_window_seconds) || DEFAULT_RAPID_FIRE_WINDOW_SECS
end

.record_request(intent_hash) ⇒ Object



24
25
26
27
28
29
# File 'lib/legion/mcp/context_guard.rb', line 24

def record_request(intent_hash)
  mutex.synchronize do
    requests[intent_hash] ||= []
    requests[intent_hash] << Time.now
  end
end

.requestsObject



98
99
100
# File 'lib/legion/mcp/context_guard.rb', line 98

def requests
  @requests ||= {}
end

.reset!Object



31
32
33
# File 'lib/legion/mcp/context_guard.rb', line 31

def reset!
  mutex.synchronize { requests.clear }
end

.setting(key) ⇒ Object



77
78
79
80
81
82
# File 'lib/legion/mcp/context_guard.rb', line 77

def setting(key)
  Legion::Settings.dig(:mcp, :tier0, :guards, key)
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'legion.mcp.context_guard.setting')
  nil
end

.stale?(pattern) ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
38
39
40
# File 'lib/legion/mcp/context_guard.rb', line 35

def stale?(pattern)
  last_hit = pattern[:last_hit_at]
  return false unless last_hit

  (Time.now - last_hit) > max_stale_seconds
end

.staleness_failure(pattern) ⇒ Object



84
85
86
87
# File 'lib/legion/mcp/context_guard.rb', line 84

def staleness_failure(pattern)
  age = pattern[:last_hit_at] ? (Time.now - pattern[:last_hit_at]).round(0) : 0
  { passed: false, guard: :staleness, reason: "pattern stale (#{age}s since last hit)" }
end