Fail Closed Without Blocking Everything
A denial is useful only when it is correct
Fail closed means that when a policy turns out to be invalid, unreachable, or unavailable for some reason, the system responds by preventing a sensitive action rather than defaulting to allowing it through. That's clearly the right default for anything touching credential exposure or real tool execution - an unenforceable policy shouldn't just get silently bypassed. For a comparatively low-risk case like ordinary chat, though, a clearly labeled "service temporarily unavailable" response may genuinely be a better user experience than taking the entire service down over one malfunctioning policy check - the right choice depends on how much is actually at stake in each specific case.
Build a concrete decision table covering every case you can think of: allowed ordinary input, input that should trigger the blocked-injection pattern, deliberately oversized input, valid output that should pass cleanly, malformed JSON that should be rejected, output missing a required key, and output using a forbidden enum value. For every single case, assert both the overall allowed/blocked decision and the specific reason code returned - checking only the final decision without checking the reason can hide a rule firing for the wrong cause even when the end result happens to look correct. It's also worth explicitly testing policy reloads and deliberately-bad regex compilation as their own scenarios, before ever deploying a policy change to production.
Warning: No blocklist, however carefully built, can ever enumerate every conceivable harmful request in advance - new attack phrasings and edge cases keep appearing. Configuration-based rules like the ones covered here are one layer among several that need to work together: authentication, authorization, rate limiting, tool allowlists (from the terminal-agents module), output validation, and human approval for genuinely high-impact actions all still matter alongside this rule engine, not instead of it.
Version your rule files right alongside their corresponding tests, require a real review for any policy change before it ships (the same discipline you'd apply to a code change), and record which specific policy version was active for every decision you make, so a later investigation can reconstruct exactly what rule set was in force at the time. Monitor both the overall rejection rate and the false-positive rate over time - without ever logging the actual sensitive content that triggered either one, echoing the redaction discipline from the previous module.
Goal: Fix only rules.json in the guardrails lab - bound the input length, block the specified injection pattern, require both output fields to be present, forbid the debug status value, and confirm ordinary legitimate input and genuinely valid output both continue to work correctly throughout.