Skip to content

The Guardrail Engine

Allen Byrd edited this page Jun 2, 2026 · 1 revision

The Guardrail Engine

The engine lives in src/regrails/guardrail.py. It is the part of RegRails that decides — deterministically, before any LLM call. Given a typed ConsultationRequest and the encoded rules, decide() returns a GuardrailDecision carrying an outcome, a list of CFR citations, a risk tier, and a human_gate_required flag.

This page covers how decide() works, the seven outcomes and when each fires, the risk tiers and the human-gate rule, the FERPA-vs-Title-IV dispatch, and the tag-based intercept logic.

See also: Architecture for where the engine sits in the data flow, Frameworks-and-Rules for the rules it consults.


How decide() works

decide(req, rules) does four things, in order:

  1. Dispatch by topic. The ConsultationRequest.topic field selects the regulatory domain:
    • other → short-circuits to out_of_scope (no rule applies; framework = "n/a").
    • aid_status → the Title IV cascade (framework = "Title IV").
    • disclosure / unknown → the FERPA cascade (framework = "FERPA").
  2. Walk the cascade. The selected cascade is a fixed, ordered sequence of if checks (a "first match wins" ladder). It returns a (outcome, citations, response_lines) tuple. The first matching rung decides; nothing below it runs.
  3. Assign risk tier + human gate. _risk_for(outcome, framework, citations) maps the outcome to a (risk_tier, human_gate_required) pair (the rule is below).
  4. Build the decision. decide() assembles a GuardrailDecision with a fresh UUID, the query, framework, timestamp, the matched-rule ids, the outcome, the risk tier, the gate flag, the citations, the rendered response lines (the deterministic rationale, not an LLM output), and a measured latency_ms. If an audit sink is supplied, it emits a GUARDRAIL_CONSULTED event plus an outcome-specific event into the hash-chained log.

The decision is 100% deterministic and reproducible without any model. The LLM (in llm.py) only runs afterward to phrase the user-facing reply, and it cannot alter the outcome.


The seven outcomes

The Outcome literal set is defined in models.py:

Outcome Meaning Fires when (representative trigger)
allow The action may proceed, subject to any cited obligation. A FERPA exception is satisfied (emergency, parent audit-log inspection, verified studies agreement, verified vendor-as-school-official, financial-aid disclosure, de-identified release, or consent on file); or a Title IV informational question (SAP explainer, warning/probation status).
block The action is prohibited; a remediation path is cited. FERPA redisclosure without consent (§ 99.33); SSN combined with a directory request (§ 99.37(e)); a directory request for a student who opted out (§ 99.37); or the default deny when no § 99.31 exception applies and no consent is on file (§ 99.30).
escalate_consent Needs written consent or a contractual condition first. The studies exception's four cumulative conditions are not all on file (§ 99.31(a)(6)); or the outsourced-vendor safe-harbor's three conditions are not all met (§ 99.31(a)(1)(i)(B)).
escalate_directory_check Needs a per-student opt-out lookup first. A directory-information request where the student's opt-out status has not been confirmed (§ 99.37).
escalate_human_review Irreversible / high-stakes — mandatory human gate. Title IV: the student is in default (§ 668.32(g)(1)); a failed SAP evaluation and an aid determination is requested (§ 668.34(a)(7)); or any other eligibility determination request (§ 668.32).
insufficient_facts A regulated matter, but key facts are missing. Title IV: an aid determination is requested but SAP status is unknown and default status is None (§ 668.34(a)(3)).
out_of_scope Not a regulated student-data / aid matter. topic == "other" — e.g. "what time does the library close?"

The web platform's recorded demo shows one representative scenario for each outcome end to end (query → outcome → risk → gate → citation).


Risk tiers and the human-gate rule

Each outcome maps to a risk tier — low, medium, or high — derived from reversibility and blast radius. The mapping is implemented in _risk_for():

Condition Risk tier human_gate_required
outcome == "escalate_human_review" high true
outcome == "block" medium false
outcome in {escalate_consent, escalate_directory_check} medium false
outcome in {insufficient_facts, out_of_scope} low false
outcome == "allow" and framework == "Title IV" medium false
outcome == "allow" and a citation contains 99.36 (health/safety emergency) medium false
outcome == "allow" (otherwise) low false

The governance stance encoded here:

  • low / reversible → safe to automate. Explaining a rule, an out-of-scope question, an aggregate de-identified release.
  • medium → automate with a cited obligation or a quick check. A consent defect, a directory opt-out lookup, a recordkeeping duty, or any Title IV informational answer (which always carries a "the institution decides specifics" caveat).
  • high / irreversible → mandatory human gate. escalate_human_review is the only outcome that sets human_gate_required = true. Loss of aid eligibility, loan default, and yes/no eligibility determinations all route here. The engine refuses to let the AI answer and directs the student to the financial-aid office (a human).

The human gate is therefore a hard, deterministic property of the outcome — not a probabilistic judgment and not something the LLM can override. In the Title IV encoding, § 668.34(a)(7) (SAP termination) and § 668.32(g)(1) (default) are the clauses that drive the high tier and force the gate.

This is a design stance, not a guarantee. Tiers are assigned per rule by the author; the engine enforces the stance deterministically, but it cannot promise every real-world high-stakes situation has been classified as high. A miscategorized rule would route a serious case as medium. See the methodology/limitations notes (docs/METHODOLOGY.md §6–7).


FERPA vs. Title IV dispatch

The two cascades occupy disjoint logic and disjoint tag spaces, so a query is settled entirely within one framework.

FERPA cascade (_decide_ferpa)

Selected for topic disclosure or unknown. The ladder, in order (first match wins):

  1. Emergencyemergency_request tag + emergency_justifiedallow (§ 99.36 health/safety) with the § 99.32(a)(5) recordkeeping obligation surfaced.
  2. Redisclosure default-blockredisclosure_request without consent → block (§ 99.33).
  3. Parent / eligible-student audit-log inspectionallow (§ 99.32(c)(1)).
  4. SSN combined with directory datablock (§ 99.37(e) + § 99.30).
  5. Directory-information path — if the student opted out → block (§ 99.37); otherwise → escalate_directory_check (confirm opt-out status first).
  6. Studies / research — if de-identified → allow (§ 99.31(b)(1)); else if all four cumulative § 99.31(a)(6) conditions are on file → allow; otherwise → escalate_consent listing the missing conditions.
  7. Vendor as school official — if all three cumulative § 99.31(a)(1)(i)(B) safe-harbor conditions are met → allow; otherwise → escalate_consent listing what's missing.
  8. Financial-aid disclosure (FERPA's § 99.31(a)(4) prong) → allow.
  9. Consent on fileallow (§ 99.30).
  10. Default block — nothing matched and no consent → block (§ 99.30(a)). This is the deny-by-default backstop.

Title IV cascade (_decide_title_iv)

Selected for topic aid_status. The ladder, in order:

  1. Loan defaultstudent_in_default is Trueescalate_human_review / high / gated (§ 668.32(g)(1)).
  2. Failed SAP + determination askedsap_status == "failed_eval" and aid_determination_requestedescalate_human_review / high / gated (§ 668.34(a)(7)).
  3. Determination asked but SAP unknown and default unknowninsufficient_facts (§ 668.34(a)(3)).
  4. Financial-aid warningsap_status == "on_warning"allow, informational (§ 668.34(a)(8)(i)).
  5. Financial-aid probationsap_status == "on_probation"allow, informational (§ 668.34(a)(8)(ii)); if an appeal basis is present, also cites § 668.34(a)(9)(ii).
  6. Any other eligibility determination requestedescalate_human_review / high / gated (§ 668.32).
  7. General informational questionallow (§ 668.34 + § 668.32), with the "the financial-aid office makes specific determinations" caveat.

The asymmetry is deliberate: Title IV advice (explaining SAP, warning, probation) automates; Title IV determinations (default, SAP termination, yes/no eligibility) route to a human.


Intercept logic: how a query reaches a rung

Routing inside each cascade is driven by deterministic tag derivation, not by reading the free-text query with a model. derive_tags(req) computes a set[str] of rule-trigger tags purely from the structured ConsultationRequest fields:

  • topic == "other" → the empty set (no tags, so out_of_scope).
  • topic == "aid_status" → Title IV tags (aid_status_query, sap_question, aid_eligibility, plus aid_determination, sap_termination, loan_default, sap_appeal, etc., conditioned on sap_status, student_in_default, aid_determination_requested, appeal_basis_present, and purpose).
  • FERPA path → pii_request, default_consent_gate, disclosure, then role-derived tags (school_officialschool_official_request; outsourced_vendorvendor_request + ai_vendor; parentparent_request; researcherstudies_request; emergency_responderemergency_request; law_enforcementsubpoena_request; third_partythird_party_forward), purpose-derived tags, and data-derived tags.

Data items are classified against two fixed sets: a directory set (name, address, phone, major, dates of attendance, athletic-roster fields, etc.) and a non-directory set (GPA, transcript, test scores, SSN, disciplinary records, health records, etc.). A non-directory item raises non_directory_request; an SSN alongside other data raises ssn_combined; directory-only items raise directory_info_request.

Two consequences:

  • match_rules(req, rules) returns every encoded rule whose triggers intersect the derived tags — that's the matched_rules list on the decision, the link back to the rule catalog. FERPA and Title IV tags never overlap, so the matcher never cross-matches frameworks.
  • The cascade ladders read these tags (and a few raw boolean fields like consent_on_file and emergency_justified) to pick the rung.

Because tag derivation is pure and total, the same ConsultationRequest always yields the same tags, the same matched rules, and the same outcome — which is exactly what the golden corpus asserts on. The honest tradeoff: a real query phrased outside the modeled tags can land on the wrong cascade, or fall through to the deny-by-default rung, for the wrong reason (see docs/METHODOLOGY.md §7, failure mode 2).

Clone this wiki locally