Class: Insika::Safety::InputGuardrail

Inherits:
Middleware
  • Object
show all
Defined in:
lib/insika/safety/input_guardrail.rb

Overview

Input guardrail as a Middleware (RFC-0009 ยง3.1). It sits on the ONE seam that already short-circuits structurally (stage 4: a link that does not call nxt), and uses the NEW graceful-halt contract: instead of halt_reason (which the Executor maps to a turn FAILURE), it sets halt_response (a safe reply) + guardrail_block (audit metadata) and returns without calling nxt. The Executor completes the turn with that safe reply, never touching the LLM.

Two tiers (D2):

1. deterministic scan (always, cheap, zero-token) โ€” Detectors#scan_input;
2. LLM moderator (opt-in per agent) โ€” only when the deterministic tier let
 the message through, so the cheap layer short-circuits the expensive one.

Auto-disables per turn by reading guardrails: off the profile (Config): the link lives ONCE in the global MiddlewareStack โ€” there is no per-agent stack.

moderator_factory (optional): ->(config) { Moderator | nil }, built by the Safety::Factory. nil = deterministic only (Fase A/B parity).

Instance Method Summary collapse

Constructor Details

#initialize(moderator_factory: nil) ⇒ InputGuardrail

Returns a new instance of InputGuardrail.



28
29
30
# File 'lib/insika/safety/input_guardrail.rb', line 28

def initialize(moderator_factory: nil)
  @moderator_factory = moderator_factory
end

Instance Method Details

#call(state, &nxt) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/insika/safety/input_guardrail.rb', line 32

def call(state, &nxt)
  config = Config.from_profile(state.profile)
  return nxt.call(state) unless config.input

  hit = Detectors.scan_input(state.message.to_s, categories: config.input_categories)
  return block(state, config, category: hit[:category], source: :deterministic, detail: hit[:matched]) if hit

  if config.moderator? && (mod = build_moderator(config))
    verdict = mod.classify(state.message.to_s)
    if verdict.block?
      category = moderator_category(verdict)
      return block(state, config, category: category, source: :moderator,
                                  detail: verdict.reason, action: verdict.action)
    end
  end

  nxt.call(state)
end