@demystify/ai-guardrails
v0.3.1
Published
Input/output safety for LLM calls: checksum-gated Indian PII detection and redaction (Aadhaar/PAN/GSTIN/IFSC/phone/account), prompt-injection detection scoped to untrusted spans, and a configurable policy. Zero dependencies, offline, pure. Leaf package.
Downloads
674
Maintainers
Readme
@demystify/ai-guardrails — input/output safety for LLM calls
PII detection and redaction before text reaches a prompt, plus prompt-injection detection scoped to untrusted spans.
Pure, zero runtime dependencies, offline, no clock, no model call. Safe to run synchronously in front of every prompt on a serverless path.
Install
pnpm add @demystify/ai-guardrailsQuickstart
import { guardUntrusted } from "@demystify/ai-guardrails";
// OCR'd invoice text, about to be interpolated into a prompt.
const { text, spans, hasFindings } = guardUntrusted(ocrText);
text; // PII masked, ready for the prompt
spans[0].pii; // [{ kind: "gstin", … }, { kind: "phone_in", … }]
spans[0].flagged // true if the document tried to talk to your modelMulti-span, when you are assembling a prompt from mixed sources:
import { applyPolicy } from "@demystify/ai-guardrails";
const result = applyPolicy(
[
{ text: systemInstructions, trust: "trusted" },
{ text: ocrText, trust: "untrusted" },
{ text: whatsappBody, trust: "untrusted" },
],
{ onInjection: "strip" },
);Two design decisions worth knowing
1. Trust is per-span, never per-message
Your own system prompt legitimately contains override-shaped language ("ignore any previous formatting instructions"). Scanning a whole prompt flags it, and a detector that cries wolf on your own instructions gets switched off. So:
untrusted— OCR'd documents, inbound WhatsApp/email bodies, retrieved pages, user-uploaded CSVs. Scanned for injection.trusted— your system instructions. Not scanned.- Unlabelled spans default to untrusted: a span whose provenance nobody recorded is not one to take on faith.
PII is redacted in every span regardless. Trust is about instruction authority, not about whether an Aadhaar number may be sent to a provider.
2. Detection is checksum-gated
A detector that fires on every 12-digit run redacts invoice numbers, order ids and concatenated phone numbers as if they were Aadhaar. A team whose real data keeps getting mangled turns redaction off, and a guardrail that is off protects nothing.
| Identifier | Gate |
|---|---|
| Aadhaar | Verhoeff check digit + first digit 2–9 (UIDAI issues no 0/1 prefixes) |
| GSTIN | mod-36 check character, valid state code, and a valid embedded PAN |
| PAN | structure + holder-type character (4th char) |
| IFSC | structure (no checksum exists) |
| Phone (IN) | +91 / leading-0 / bare, spaced or hyphenated |
| Email | structure |
| Bank account | 9–18 digits — the one detector that must guess. Runs last, claims only spans no other detector took, and can be disabled via piiKinds. |
GSTIN embeds a PAN, so ordering is the resolution rule: the first detector to claim a span wins.
Masks keep the last four digits where that is permitted and useful — UIDAI allows displaying the last four of an Aadhaar, and "which account was that?" is unanswerable without them. A PAN is removed whole; no part of it is safe to show.
Injection signals
instruction_override · role_impersonation · exfiltration · tool_coercion ·
delimiter_break · encoded_payload
The score saturates toward 1 rather than summing past it, and each signal counts
once — fifty copies of the same trick is one trick. Default threshold 0.4.
onInjection: annotate (default, you decide) · block (refuse the input) ·
strip (drop the flagged span).
This is heuristic and says so. It raises the cost of an attack; it does not make injection impossible. The durable defence is never granting an untrusted span authority in the first place — keep it in a user-role message, never a system one, and never let it choose a tool.
DPDP posture
The contract is raw PII never reaches a provider, which is stronger and far more testable than a retention promise. Redaction happens before the prompt is built, not before the log is written.
Findings carry the matched value so callers can act on it. Do not log that field.
Testing
pnpm test # 33 tests100% statements / 94% branches. The negative tests matter most: an ordinary amount, a year, an invoice number and a PAN-shaped reference that fails the holder-type rule must all survive untouched.
Status
New in the Demystify substrate — nothing in the federation had PII redaction or
injection defence before this. A Python twin (demystify-ai-guardrails) is next: the
extraction worker is Python and OCR'd text reaching a prompt is exactly the untrusted
span this package exists for.
MIT.
