Class: Vangrail::Engine
- Inherits:
-
Object
- Object
- Vangrail::Engine
- Defined in:
- lib/vangrail/engine.rb
Overview
Runs ordered rails over text and reports one Result.
The rules are short enough to state in full:
- Rails run in the order given. The first :blocked ends the pass.
- A :modified result replaces the text for every rail after it, and the engine reports :modified unless something later blocks.
- A rail that raises is not a rail that passed.
on_error: :allow(the default) keeps going and marks the pass uncertain;:blockstops. - An empty rail list returns :passed with certain false. Nothing ran.
Threading rewrites through later rails is the part worth being explicit about: a redaction rail that runs before a policy rail should have the policy rail judge the redacted text, not the original.
Defined Under Namespace
Instance Attribute Summary collapse
-
#cache ⇒ Object
readonly
Returns the value of attribute cache.
-
#context_rails ⇒ Object
readonly
Returns the value of attribute context_rails.
-
#input_rails ⇒ Object
readonly
Returns the value of attribute input_rails.
-
#on_error ⇒ Object
readonly
Returns the value of attribute on_error.
-
#output_rails ⇒ Object
readonly
Returns the value of attribute output_rails.
Instance Method Summary collapse
-
#assess(text, side: :input, prior: nil, policy: Policy::DEFAULT, evidence: EvidenceData::TABLE, escalate: false, confidence: nil, origin: nil, **context) ⇒ Object
How likely is it that this text is an attack, given everything that ran.
-
#check_context(text, **context) ⇒ Object
One retrieved document, before it goes anywhere near a prompt.
- #check_input(text, context = {}) ⇒ Object
- #check_output(text, user_input: nil, passages: nil, **context) ⇒ Object
- #describe ⇒ Object
- #empty? ⇒ Boolean
-
#initialize(input: [], context: [], output: [], on_error: :allow, cache: true) ⇒ Engine
constructor
A new instance of Engine.
-
#offline? ⇒ Boolean
True when every configured rail decides without a network call, which is the only case where an unreachable endpoint cannot weaken the check.
- #rail_names(side) ⇒ Object
- #rails(side) ⇒ Object
-
#screen(documents, **context) ⇒ Object
Screens a set of retrieved documents and reports what survived.
- #to_h ⇒ Object
-
#triage(documents, prior:, policy: Policy::DEFAULT, escalate: false, **context) ⇒ Object
Screening, with the documents ranked by how suspicious they are rather than partitioned by whether one rail objected.
Constructor Details
#initialize(input: [], context: [], output: [], on_error: :allow, cache: true) ⇒ Engine
Returns a new instance of Engine.
30 31 32 33 34 35 36 37 38 |
# File 'lib/vangrail/engine.rb', line 30 def initialize(input: [], context: [], output: [], on_error: :allow, cache: true) @input_rails = Array(input) @context_rails = Array(context) @output_rails = Array(output) @on_error = on_error.to_sym raise ArgumentError, 'on_error must be :allow or :block' unless %i[allow block].include?(@on_error) @cache = cache.is_a?(ResultCache) ? cache : (ResultCache.new if cache) end |
Instance Attribute Details
#cache ⇒ Object (readonly)
Returns the value of attribute cache.
28 29 30 |
# File 'lib/vangrail/engine.rb', line 28 def cache @cache end |
#context_rails ⇒ Object (readonly)
Returns the value of attribute context_rails.
28 29 30 |
# File 'lib/vangrail/engine.rb', line 28 def context_rails @context_rails end |
#input_rails ⇒ Object (readonly)
Returns the value of attribute input_rails.
28 29 30 |
# File 'lib/vangrail/engine.rb', line 28 def input_rails @input_rails end |
#on_error ⇒ Object (readonly)
Returns the value of attribute on_error.
28 29 30 |
# File 'lib/vangrail/engine.rb', line 28 def on_error @on_error end |
#output_rails ⇒ Object (readonly)
Returns the value of attribute output_rails.
28 29 30 |
# File 'lib/vangrail/engine.rb', line 28 def output_rails @output_rails end |
Instance Method Details
#assess(text, side: :input, prior: nil, policy: Policy::DEFAULT, evidence: EvidenceData::TABLE, escalate: false, confidence: nil, origin: nil, **context) ⇒ Object
How likely is it that this text is an attack, given everything that ran.
check_input and friends answer a different question. They run the rails
in order, stop at the first block, and report a decision; that is the
right shape for a request path and it is what every published defence
does. It also throws away most of what was measured. A rail that fired
tells you nothing about how much that hit is worth, three rails that
nearly fired tell you nothing at all, and a block carries no number an
operator can set a policy against.
This runs every rail that has a measured operating point, treats each verdict as evidence, and combines it with the deployment's base rate. What comes back is a probability, the bits each rail contributed, and an action under a stated policy.
The prior is not optional and has no sensible default. Detector papers report their numbers on balanced corpora, where an attack is half the traffic; a documentation desk over an editable wiki might see one poisoned page in ten thousand. Those two worlds disagree about what a hit means by four orders of magnitude, and only the deployment knows which one it is in. Guessing on its behalf would be the whole error this method exists to expose.
judgement = engine.assess(page, side: :context, prior: 1e-4)
judgement.posterior # => 0.0073
judgement.action # => :review
judgement.fired # => [{rail: "paraphrase", bits: 6.2, ...}]
Costs more than a check, because nothing short-circuits: every rail with an entry in the table runs, including the ones a block would have skipped.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/vangrail/engine.rb', line 133 def assess(text, side: :input, prior: nil, policy: Policy::DEFAULT, evidence: EvidenceData::TABLE, escalate: false, confidence: nil, origin: nil, **context) raise ArgumentError, if prior.nil? origin = Origin.coerce(origin || Origin.default_for(side)) observed = observe(text, side, context, evidence, escalate ? { prior: prior, policy: policy } : nil) observations, direct, certain, skipped = observed posterior, contributions = Posterior.combine(prior: prior, observations: observations, evidence: evidence, direct: direct, confidence: confidence) action = policy.action_for(posterior) # A confidence bound is what this corpus can defend. If the point # estimate and the bound disagree about the action, the action is # not identified: reporting it as a certain decision would spend # the unmeasured tail of 48 benign pages. if confidence point, = Posterior.combine(prior: prior, observations: observations, evidence: evidence, direct: direct) certain &&= policy.action_for(point) == action end Judgement.new(posterior: posterior, prior: prior, bits: contributions.sum { |c| c[:bits] }, contributions: contributions, certain: certain, side: side.to_sym, skipped: skipped, action: action, origin: origin) end |
#check_context(text, **context) ⇒ Object
One retrieved document, before it goes anywhere near a prompt.
49 50 51 |
# File 'lib/vangrail/engine.rb', line 49 def check_context(text, **context) run(:context, context_rails, text, context) end |
#check_input(text, context = {}) ⇒ Object
40 41 42 |
# File 'lib/vangrail/engine.rb', line 40 def check_input(text, context = {}) run(:input, input_rails, text, context) end |
#check_output(text, user_input: nil, passages: nil, **context) ⇒ Object
44 45 46 |
# File 'lib/vangrail/engine.rb', line 44 def check_output(text, user_input: nil, passages: nil, **context) run(:output, output_rails, text, context.merge(user_input: user_input, passages: passages)) end |
#describe ⇒ Object
246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/vangrail/engine.rb', line 246 def describe return 'no rails' if empty? parts = [] parts << "input=#{rail_names(:input).join('+')}" unless input_rails.empty? parts << "context=#{rail_names(:context).join('+')}" unless context_rails.empty? parts << "output=#{rail_names(:output).join('+')}" unless output_rails.empty? parts << "on_error=#{on_error}" parts << 'offline' if offline? parts.join(' ') end |
#empty? ⇒ Boolean
231 232 233 |
# File 'lib/vangrail/engine.rb', line 231 def empty? input_rails.empty? && context_rails.empty? && output_rails.empty? end |
#offline? ⇒ Boolean
True when every configured rail decides without a network call, which is the only case where an unreachable endpoint cannot weaken the check.
226 227 228 229 |
# File 'lib/vangrail/engine.rb', line 226 def offline? all = input_rails + context_rails + output_rails !all.empty? && all.all?(&:offline?) end |
#rail_names(side) ⇒ Object
220 221 222 |
# File 'lib/vangrail/engine.rb', line 220 def rail_names(side) rails(side).map(&:name) end |
#rails(side) ⇒ Object
212 213 214 215 216 217 218 |
# File 'lib/vangrail/engine.rb', line 212 def rails(side) case side.to_sym when :input then input_rails when :context then context_rails else output_rails end end |
#screen(documents, **context) ⇒ Object
Screens a set of retrieved documents and reports what survived.
A document that fails is dropped rather than failing the whole turn. One poisoned wiki page should cost a reader that page, not their answer, and an application that refuses outright teaches its readers that the guardrail is the problem.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/vangrail/engine.rb', line 59 def screen(documents, **context) kept = [] rejected = [] uncertain = nil Array(documents).each_with_index do |document, index| result = check_context(text_of(document), **context, document: document, index: index) uncertain ||= result unless result.certain? if result.blocked? rejected << { document: document, result: result } else kept << (result.modified? ? replace_text(document, result.content) : document) end end Screening.new(kept: kept, rejected: rejected, certain: uncertain.nil?, reason: uncertain&.reason) end |
#to_h ⇒ Object
235 236 237 238 239 240 241 242 243 244 |
# File 'lib/vangrail/engine.rb', line 235 def to_h { 'input' => rail_names(:input), 'context' => (rail_names(:context) unless context_rails.empty?), 'output' => rail_names(:output), 'on_error' => on_error.to_s, 'offline' => offline?, 'cache' => cache&.to_h, }.compact end |
#triage(documents, prior:, policy: Policy::DEFAULT, escalate: false, **context) ⇒ Object
Screening, with the documents ranked by how suspicious they are rather than partitioned by whether one rail objected.
screen drops a document the moment a rail blocks it, which is the right
shape when a rail is a switch. Given a posterior there is a better answer
available: rank the set, drop what the policy says to drop, hand what it
says to review to whoever reviews, and keep the rest. A page that trips
one pattern at a base rate of one in ten thousand is not a page worth
taking away from a reader, and it is worth putting at the bottom of the
passage list.
triage = engine.triage(documents, prior: 1e-4)
triage.keep # documents, least suspicious first
triage.review # [{document:, judgement:}]
triage.dropped # [{document:, judgement:}]
173 174 175 176 177 178 179 180 |
# File 'lib/vangrail/engine.rb', line 173 def triage(documents, prior:, policy: Policy::DEFAULT, escalate: false, **context) judged = Array(documents).each_with_index.map do |document, index| judgement = assess(text_of(document), side: :context, prior: prior, policy: policy, escalate: escalate, **context, document: document, index: index) { document: document, judgement: judgement } end Triage.new(judged: judged.sort_by { |row| -row[:judgement].posterior }) end |