Guardrails
The chokepoint between your app and the model, moderate, redact, and block, fail-closed by default.
What it is
Guardrails is the fail-closed input/output guard around a model call: guardInput moderates then redacts PII on the way in, guardOutput moderates on the way out, and either leg throws a 422 GuardrailError on a block instead of letting a moderator outage pass content through silently. A swappable Moderator port (local regex, provider, or custom) backs the moderation call; an unconditional secret-shape gate runs before it on either leg, no opt-out.
What ships in the module
Fail-closed by default
guardInput and guardOutput throw GuardrailError (HTTP 422) on any block. A moderator outage or timeout fails closed unless the policy explicitly sets failOpen: true, the default protects the request, not the moderator's uptime.
Unconditional secret-shape gate
Before either leg reaches a moderator, guard.ts checks looksLikeSecret(text) and blocks category "secret" with no policy field and no opt-out (ADR-0215), a leaked credential never gets a moderation call, live or not.
Swappable Moderator port
localModerator runs a zero-network regex blocklist; providerModerator wraps an injected async check for a real vendor call; customModerator hooks in your own function. All three implement the one-method Moderator interface guard.ts calls under moderateWithDeadline.
Three PII redaction modes
detectPii finds email, SSN, Luhn-validated credit card, and phone spans. redactPii replaces them irreversibly (mask → [EMAIL], hash → [EMAIL:ab12…]); tokenizePii instead seals the original via field-crypto and swaps in an opaque placeholder that detokenizePii can restore after the round trip.
FTC “4 Ps” dark-pattern evaluator
evaluateFtc4P scores marketing/UI copy against five rule classes (false urgency, forced continuity, confirmshaming, opt-out enrollment, drip pricing) charted across four dimensions (prominence, presentation, placement, proximity). Wrap it as a Moderator with ftc4pModerator to gate guardOutput on your own copy.
Metadata-only blocked event
Every block emits a guardrail.blocked event to the kernel EventSink carrying blockId, stage, category, policy, and failClosed, never the flagged text. The emit is fire-and-forget: a telemetry-sink failure can't mask or delay the block itself.
// Unconditional credential-shape gate (ADR-0215) — runs BEFORE the (possibly outaged/provider)
// moderator, reusing the ONE `looksLikeSecret` predicate (kernel). No policy field, no opt-out: a
// raw credential in either leg never reaches a moderator call, live or not.
if (looksLikeSecret(text)) block(stage, "secret", false, policy, rt);
let result: ModerationResult;
try {
result = await moderateWithDeadline(
policy.moderator,
text,
policy.timeoutMs ?? DEFAULT_TIMEOUT_MS,
);
} catch {
// Outage / timeout / driver throw → fail-closed unless the operator explicitly opted out.
if (policy.failOpen === true) return;
block(stage, "moderation", true, policy, rt);
}
if (result.flagged) block(stage, result.category, false, policy, rt);- looksLikeSecret runs before the moderator call, live or not, a leaked credential never becomes a moderation API call.
- The try/catch around moderateWithDeadline is where fail-closed lives: only an explicit failOpen: true on the policy lets an outage pass content through instead of blocking.