Class: Insika::Safety::InputGuardrail

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

Overview

Input guardrail as a Middleware. 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:

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 (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
50
51
52
53
54
# 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
    # an UNAVAILABLE moderator fails open — the turn proceeds —
    # but silence is not a negative: record the degradation on the state so the
    # Executor (single emitter) emits :guardrail_flagged and a degraded tier
    # never looks identical to a healthy one in the audit stream.
    flag_unavailable(state, verdict) if verdict.unavailable?
  end

  nxt.call(state)
end