The gaps a stateless single-message filter structurally cannot see — each shipped default-off or behind a new subpath so the zero-config Tier-0 path, the CI gates (benign FPR < 1%, NotInject < 5%, attack recall ≥ 90%, hard-block 100%, Tier 0 p99 < 1ms), and the eval corpora are all unchanged.
For the companion-based hardening (canary, taint, session), see Companions. This page covers the Tier-0/Tier-1 detection upgrades.
normalize.neutralizeEncoded (default 'off') closes the detect→model gap: today a
decoded blob is detected but the original encoded blob still ships in sanitized —
a downstream model decodes and obeys it. Detection without mitigation.
Set to:
'strip'— remove the offending blob span fromsanitized(the model copy)'spotlight'— wrap it with the datamark marker so it reads as inert data
const guard = createGuard({
normalize: { neutralizeEncoded: 'strip' },
});FP discipline: only fires on blobs that themselves re-scan as injection — benign
base64 (images, hashes) is untouched. Default 'off', fully backward compatible. Emits
encoded_payload_neutralized and sets GuardResult.neutralized = true.
The matching copy is never touched (R4 two-copy invariant preserved); only
sanitized (the model copy) is rewritten, and only to remove an attack payload, never
to alter legitimate content. The blob is located by literal substring match (exact for
ASCII base64/hex, the common case).
normalize.specialTokens (default: a Llama/Qwen/GPT/Mistral/Gemma control-token list)
scans the matching copy → special_token_injection. Control tokens (<|im_start|>,
[INST], <<SYS>>, <start_of_turn>, <|eot_id|>, …) have essentially zero
legitimate use in untrusted user data — they're how an attacker forges chat-template
turns.
const guard = createGuard({
normalize: {
specialTokens: ['<|im_start|>', '<|im_end|>', '[INST]', '[/INST]', '<my-custom-token>'],
},
});- Matching copy only — the model copy is untouched (R4).
- A cheap
</[pre-check keeps the always-on Tier-0 path fast: if the input contains neither char and every configured token starts with one of them, the 20-branch alternation regex is skipped entirely. - Weight is moderate (rises with match count) so it raises attacker cost without an
FP-driven block on benign prose that happens to type
<|...|>.
The default list is intentionally full, unambiguous tokens only — no partial prefixes or model names that risk flagging benign mentions.
normalize.scanAdversarialSuffix (default false) → low-weight adversarial_suffix.
A zero-LM proxy for optimizer-generated (GCG) suffixes: optimizer suffixes read as
garbage to humans but flip models. The design deliberately excluded perplexity (no LM on
the hot path) — this is a near-free proxy instead.
const guard = createGuard({
normalize: { scanAdversarialSuffix: true },
});The signal is calibrated to 0 benign FP on code blocks, base64, hashes, and JSON.
A "salad run" is a whitespace-free run that is mostly letters, contains an embedded
punctuation/symbol char (NOT a pure base64/hex blob), has no base64-length alnum
subrun, and whose letter-trigrams are mostly NOT common English. GCG suffixes stitch
several such word-fragment-with-punctuation tokens; a single one looks like a code
identifier, so the signal requires ≥ 3 salad runs in the same input — this is what
separates real token-salad from a normal import { foo } from "bar" line.
Low weight, escalation signal only — routes to Tier 1, never blocks on its own.
LocalModelDetector.smoothing: { n, perturbation } runs n lightly-perturbed copies
through the classifier and takes the mean score. Adversarial suffixes (GCG) are tuned to
an exact string and are brittle to perturbation; benign text is not. For a
classifier-evasion suffix (exact→benign, perturbed→injection) the mean reveals the
injection; for a brittle high-on-exact signal (perturbed→benign) the mean is robust and
avoids an over-flag.
const guard = createGuard({
detectors: [
{ kind: 'heuristics' },
{ kind: 'localModel', runtime: 'node', smoothing: { n: 5, perturbation: 0.1 } },
],
});| Option | Default | Notes |
|---|---|---|
n |
5 |
Number of perturbed copies |
perturbation |
0.1 |
Per-char drop/swap probability (capped at 0.5) |
Gated to highRiskAction only so the n× latency/FP cost stays off the common
(non-high-risk) path. The label is decided by majority vote (injection if > n/2
copies score > 0.5), the score by mean.
These are companion subpaths, repeated here for discoverability:
- Canary —
opensentry/canary: deterministic, near-zero-FP system-prompt-leak detection via a 128-bit nonce. A hit maps tocanary_leak(hard-block) — a confirmed extraction, not a heuristic guess. - Taint —
opensentry/taint: provenance-passing for indirect-injection defense. Wired intocheckToolCallviaopts.tracker→tainted_data_flow+ fail-closed. Low-FP because it flags data flow into privileged actions, not content. - Session —
opensentry/session: stateful multi-turn guard. Catches Crescendo / Bad Likert Judge / many-shot, which exceed ~70% success because no single turn is flaggable. Foldscumulative_risk/session_escalation/manyshot_densityvia noisy-OR; can only escalate, never de-escalate.
session_escalation, manyshot_density, cumulative_risk,
encoded_payload_neutralized, tainted_data_flow, canary_leak, secret_egress,
pii_egress, special_token_injection, adversarial_suffix. See the full
ReasonCode table.
These are structurally out of reach and documented honestly rather than pretended away:
- True automatic taint propagation — impossible in JS without language/runtime
support;
opensentry/taintships explicit provenance-passing instead, and says so. - Full perplexity scoring — excluded from the hot path (needs an LM, high latency, blind to fluent paraphrase); the adversarial-suffix signal is a zero-LM proxy, not a real LM.
- Distributed session state —
opensentry/sessionships an in-memory store + aSessionStoreinterface; the actual Redis/DB backend is the integrator's. - A harmful-content classifier in Tier 0 — rejected (re-introduces NotInject over-defense); harmful-intent detection stays Tier 1's job.
- Multi-turn / many-shot at the single-message level — structurally out of reach; the session guard addresses it.