mem-os-python 1.6.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mem_os_python-1.6.27.dist-info/METADATA +1032 -0
- mem_os_python-1.6.27.dist-info/RECORD +253 -0
- mem_os_python-1.6.27.dist-info/WHEEL +4 -0
- mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
- mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
- memos/__init__.py +16 -0
- memos/_js/adapters/anthropic.d.ts +279 -0
- memos/_js/adapters/anthropic.d.ts.map +1 -0
- memos/_js/adapters/anthropic.js +348 -0
- memos/_js/adapters/anthropic.js.map +1 -0
- memos/_js/adapters/base.d.ts +10 -0
- memos/_js/adapters/base.d.ts.map +1 -0
- memos/_js/adapters/base.js +2 -0
- memos/_js/adapters/base.js.map +1 -0
- memos/_js/adapters/openai.d.ts +264 -0
- memos/_js/adapters/openai.d.ts.map +1 -0
- memos/_js/adapters/openai.js +342 -0
- memos/_js/adapters/openai.js.map +1 -0
- memos/_js/apps/explorer.d.ts +244 -0
- memos/_js/apps/explorer.d.ts.map +1 -0
- memos/_js/apps/explorer.js +738 -0
- memos/_js/apps/explorer.js.map +1 -0
- memos/_js/citations.d.ts +65 -0
- memos/_js/citations.d.ts.map +1 -0
- memos/_js/citations.js +143 -0
- memos/_js/citations.js.map +1 -0
- memos/_js/cli-browse.d.ts +3 -0
- memos/_js/cli-browse.d.ts.map +1 -0
- memos/_js/cli-browse.js +129 -0
- memos/_js/cli-browse.js.map +1 -0
- memos/_js/cli-completion.d.ts +31 -0
- memos/_js/cli-completion.d.ts.map +1 -0
- memos/_js/cli-completion.js +340 -0
- memos/_js/cli-completion.js.map +1 -0
- memos/_js/cli-help.d.ts +18 -0
- memos/_js/cli-help.d.ts.map +1 -0
- memos/_js/cli-help.js +357 -0
- memos/_js/cli-help.js.map +1 -0
- memos/_js/cli.d.ts +28 -0
- memos/_js/cli.d.ts.map +1 -0
- memos/_js/cli.js +2880 -0
- memos/_js/cli.js.map +1 -0
- memos/_js/compression.d.ts +25 -0
- memos/_js/compression.d.ts.map +1 -0
- memos/_js/compression.js +319 -0
- memos/_js/compression.js.map +1 -0
- memos/_js/confidence-machine.d.ts +99 -0
- memos/_js/confidence-machine.d.ts.map +1 -0
- memos/_js/confidence-machine.js +222 -0
- memos/_js/confidence-machine.js.map +1 -0
- memos/_js/context-pack.d.ts +506 -0
- memos/_js/context-pack.d.ts.map +1 -0
- memos/_js/context-pack.js +1435 -0
- memos/_js/context-pack.js.map +1 -0
- memos/_js/contradictions.d.ts +160 -0
- memos/_js/contradictions.d.ts.map +1 -0
- memos/_js/contradictions.js +255 -0
- memos/_js/contradictions.js.map +1 -0
- memos/_js/dashboard.d.ts +30 -0
- memos/_js/dashboard.d.ts.map +1 -0
- memos/_js/dashboard.js +302 -0
- memos/_js/dashboard.js.map +1 -0
- memos/_js/ebbinghaus.d.ts +29 -0
- memos/_js/ebbinghaus.d.ts.map +1 -0
- memos/_js/ebbinghaus.js +32 -0
- memos/_js/ebbinghaus.js.map +1 -0
- memos/_js/embedding-queue.d.ts +190 -0
- memos/_js/embedding-queue.d.ts.map +1 -0
- memos/_js/embedding-queue.js +316 -0
- memos/_js/embedding-queue.js.map +1 -0
- memos/_js/embeddings.d.ts +187 -0
- memos/_js/embeddings.d.ts.map +1 -0
- memos/_js/embeddings.js +633 -0
- memos/_js/embeddings.js.map +1 -0
- memos/_js/entity-extraction.d.ts +54 -0
- memos/_js/entity-extraction.d.ts.map +1 -0
- memos/_js/entity-extraction.js +325 -0
- memos/_js/entity-extraction.js.map +1 -0
- memos/_js/entity-index.d.ts +93 -0
- memos/_js/entity-index.d.ts.map +1 -0
- memos/_js/entity-index.js +165 -0
- memos/_js/entity-index.js.map +1 -0
- memos/_js/event-memory.d.ts +147 -0
- memos/_js/event-memory.d.ts.map +1 -0
- memos/_js/event-memory.js +290 -0
- memos/_js/event-memory.js.map +1 -0
- memos/_js/external-import.d.ts +98 -0
- memos/_js/external-import.d.ts.map +1 -0
- memos/_js/external-import.js +669 -0
- memos/_js/external-import.js.map +1 -0
- memos/_js/fidelity.d.ts +367 -0
- memos/_js/fidelity.d.ts.map +1 -0
- memos/_js/fidelity.js +911 -0
- memos/_js/fidelity.js.map +1 -0
- memos/_js/graph-expansion.d.ts +119 -0
- memos/_js/graph-expansion.d.ts.map +1 -0
- memos/_js/graph-expansion.js +213 -0
- memos/_js/graph-expansion.js.map +1 -0
- memos/_js/graph-mermaid.d.ts +13 -0
- memos/_js/graph-mermaid.d.ts.map +1 -0
- memos/_js/graph-mermaid.js +32 -0
- memos/_js/graph-mermaid.js.map +1 -0
- memos/_js/graph.d.ts +170 -0
- memos/_js/graph.d.ts.map +1 -0
- memos/_js/graph.js +482 -0
- memos/_js/graph.js.map +1 -0
- memos/_js/harness.d.ts +63 -0
- memos/_js/harness.d.ts.map +1 -0
- memos/_js/harness.js +93 -0
- memos/_js/harness.js.map +1 -0
- memos/_js/importance.d.ts +118 -0
- memos/_js/importance.d.ts.map +1 -0
- memos/_js/importance.js +144 -0
- memos/_js/importance.js.map +1 -0
- memos/_js/index.d.ts +51 -0
- memos/_js/index.d.ts.map +1 -0
- memos/_js/index.js +53 -0
- memos/_js/index.js.map +1 -0
- memos/_js/mcp.d.ts +58 -0
- memos/_js/mcp.d.ts.map +1 -0
- memos/_js/mcp.js +1853 -0
- memos/_js/mcp.js.map +1 -0
- memos/_js/memory.d.ts +1762 -0
- memos/_js/memory.d.ts.map +1 -0
- memos/_js/memory.js +6283 -0
- memos/_js/memory.js.map +1 -0
- memos/_js/procedural.d.ts +74 -0
- memos/_js/procedural.d.ts.map +1 -0
- memos/_js/procedural.js +125 -0
- memos/_js/procedural.js.map +1 -0
- memos/_js/project-config.d.ts +68 -0
- memos/_js/project-config.d.ts.map +1 -0
- memos/_js/project-config.js +59 -0
- memos/_js/project-config.js.map +1 -0
- memos/_js/proof-of-forgetting.d.ts +57 -0
- memos/_js/proof-of-forgetting.d.ts.map +1 -0
- memos/_js/proof-of-forgetting.js +80 -0
- memos/_js/proof-of-forgetting.js.map +1 -0
- memos/_js/provenance.d.ts +122 -0
- memos/_js/provenance.d.ts.map +1 -0
- memos/_js/provenance.js +182 -0
- memos/_js/provenance.js.map +1 -0
- memos/_js/quarantine-commands.d.ts +23 -0
- memos/_js/quarantine-commands.d.ts.map +1 -0
- memos/_js/quarantine-commands.js +42 -0
- memos/_js/quarantine-commands.js.map +1 -0
- memos/_js/quarantine.d.ts +120 -0
- memos/_js/quarantine.d.ts.map +1 -0
- memos/_js/quarantine.js +873 -0
- memos/_js/quarantine.js.map +1 -0
- memos/_js/redact.d.ts +12 -0
- memos/_js/redact.d.ts.map +1 -0
- memos/_js/redact.js +26 -0
- memos/_js/redact.js.map +1 -0
- memos/_js/repl.d.ts +57 -0
- memos/_js/repl.d.ts.map +1 -0
- memos/_js/repl.js +295 -0
- memos/_js/repl.js.map +1 -0
- memos/_js/rerank-local.d.ts +61 -0
- memos/_js/rerank-local.d.ts.map +1 -0
- memos/_js/rerank-local.js +210 -0
- memos/_js/rerank-local.js.map +1 -0
- memos/_js/retain-filter.d.ts +98 -0
- memos/_js/retain-filter.d.ts.map +1 -0
- memos/_js/retain-filter.js +232 -0
- memos/_js/retain-filter.js.map +1 -0
- memos/_js/retrieval.d.ts +265 -0
- memos/_js/retrieval.d.ts.map +1 -0
- memos/_js/retrieval.js +1018 -0
- memos/_js/retrieval.js.map +1 -0
- memos/_js/revert.d.ts +77 -0
- memos/_js/revert.d.ts.map +1 -0
- memos/_js/revert.js +252 -0
- memos/_js/revert.js.map +1 -0
- memos/_js/scope.d.ts +30 -0
- memos/_js/scope.d.ts.map +1 -0
- memos/_js/scope.js +71 -0
- memos/_js/scope.js.map +1 -0
- memos/_js/semantic-screen.d.ts +152 -0
- memos/_js/semantic-screen.d.ts.map +1 -0
- memos/_js/semantic-screen.js +290 -0
- memos/_js/semantic-screen.js.map +1 -0
- memos/_js/session-hooks.d.ts +127 -0
- memos/_js/session-hooks.d.ts.map +1 -0
- memos/_js/session-hooks.js +482 -0
- memos/_js/session-hooks.js.map +1 -0
- memos/_js/storage/sqlite.d.ts +427 -0
- memos/_js/storage/sqlite.d.ts.map +1 -0
- memos/_js/storage/sqlite.js +2428 -0
- memos/_js/storage/sqlite.js.map +1 -0
- memos/_js/sync.d.ts +213 -0
- memos/_js/sync.d.ts.map +1 -0
- memos/_js/sync.js +389 -0
- memos/_js/sync.js.map +1 -0
- memos/_js/tamper-log.d.ts +98 -0
- memos/_js/tamper-log.d.ts.map +1 -0
- memos/_js/tamper-log.js +327 -0
- memos/_js/tamper-log.js.map +1 -0
- memos/_js/temporal.d.ts +92 -0
- memos/_js/temporal.d.ts.map +1 -0
- memos/_js/temporal.js +536 -0
- memos/_js/temporal.js.map +1 -0
- memos/_js/time-travel.d.ts +38 -0
- memos/_js/time-travel.d.ts.map +1 -0
- memos/_js/time-travel.js +46 -0
- memos/_js/time-travel.js.map +1 -0
- memos/_js/timestamp-format.d.ts +18 -0
- memos/_js/timestamp-format.d.ts.map +1 -0
- memos/_js/timestamp-format.js +52 -0
- memos/_js/timestamp-format.js.map +1 -0
- memos/_js/triple-extraction.d.ts +31 -0
- memos/_js/triple-extraction.d.ts.map +1 -0
- memos/_js/triple-extraction.js +116 -0
- memos/_js/triple-extraction.js.map +1 -0
- memos/_js/types.d.ts +2474 -0
- memos/_js/types.d.ts.map +1 -0
- memos/_js/types.js +20 -0
- memos/_js/types.js.map +1 -0
- memos/_js/updater.d.ts +95 -0
- memos/_js/updater.d.ts.map +1 -0
- memos/_js/updater.js +268 -0
- memos/_js/updater.js.map +1 -0
- memos/_js/version.d.ts +17 -0
- memos/_js/version.d.ts.map +1 -0
- memos/_js/version.js +34 -0
- memos/_js/version.js.map +1 -0
- memos/_js/vision/clip-query-cache.d.ts +58 -0
- memos/_js/vision/clip-query-cache.d.ts.map +1 -0
- memos/_js/vision/clip-query-cache.js +79 -0
- memos/_js/vision/clip-query-cache.js.map +1 -0
- memos/_js/vision/clip.d.ts +113 -0
- memos/_js/vision/clip.d.ts.map +1 -0
- memos/_js/vision/clip.js +224 -0
- memos/_js/vision/clip.js.map +1 -0
- memos/_js/vision/fake-clip.d.ts +55 -0
- memos/_js/vision/fake-clip.d.ts.map +1 -0
- memos/_js/vision/fake-clip.js +122 -0
- memos/_js/vision/fake-clip.js.map +1 -0
- memos/_js/vision/index.d.ts +15 -0
- memos/_js/vision/index.d.ts.map +1 -0
- memos/_js/vision/index.js +13 -0
- memos/_js/vision/index.js.map +1 -0
- memos/_js/vision/store.d.ts +137 -0
- memos/_js/vision/store.d.ts.map +1 -0
- memos/_js/vision/store.js +383 -0
- memos/_js/vision/store.js.map +1 -0
- memos/adapters/__init__.py +0 -0
- memos/adapters/crewai.py +385 -0
- memos/adapters/langchain.py +301 -0
- memos/adapters/ollama.py +396 -0
- memos/server/__init__.py +0 -0
- memos/server/main.py +547 -0
- memos/server/routes.py +360 -0
memos/_js/quarantine.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-gate quarantine — a local-heuristic classifier for
|
|
3
|
+
* instruction-like payloads in memory writes and imports.
|
|
4
|
+
*
|
|
5
|
+
* Runs synchronously on the `store()` hot path (pure regex scoring,
|
|
6
|
+
* microseconds per write — no LLM, no network). Content that scores at
|
|
7
|
+
* or above {@link QUARANTINE_FLAG_THRESHOLD} is still stored (add-only
|
|
8
|
+
* is preserved) but marked `quarantined: true`, which excludes it from
|
|
9
|
+
* recall by default until a human reviews it via
|
|
10
|
+
* `memos quarantine list|release`.
|
|
11
|
+
*
|
|
12
|
+
* The classifier first normalizes its input
|
|
13
|
+
* ({@link normalizeForScreening}): NFKC unicode folding, zero-width
|
|
14
|
+
* character stripping, Cyrillic/Greek homoglyph folding, single-newline
|
|
15
|
+
* collapsing, spaced-letter joining (`i g n o r e` → `ignore`), and
|
|
16
|
+
* whitespace collapsing. Injection clichés are additionally matched
|
|
17
|
+
* against a leetspeak-unfolded copy (`1gn0re` → `ignore`).
|
|
18
|
+
*
|
|
19
|
+
* Signal classes (each reports a stable machine-readable `code`):
|
|
20
|
+
*
|
|
21
|
+
* 1. Injection clichés — verbatim prompt-injection openers ("ignore
|
|
22
|
+
* previous instructions", "disregard your prior instructions",
|
|
23
|
+
* "do not tell the user", "pretend you are …", DAN/jailbreak
|
|
24
|
+
* patterns, chat-template role markers). Full-strength (2.0:
|
|
25
|
+
* quarantines on its own). Deliberately anchored to
|
|
26
|
+
* `previous`/`prior` so ordinary "ignore the old template" phrasing
|
|
27
|
+
* does NOT match.
|
|
28
|
+
* 2. Imperative exfiltration — an exfil verb (send, email, upload, dump,
|
|
29
|
+
* …) governing a credential noun (passwords, api keys, secrets, …).
|
|
30
|
+
* 1.5 alone; 2.0 when a URL/email destination appears anywhere in
|
|
31
|
+
* the write, or when verb+destination share a sentence and a
|
|
32
|
+
* credential noun appears anywhere. First-person possessives ("my
|
|
33
|
+
* password", "our api key") are exempt — "reset my password" is not
|
|
34
|
+
* an attack. On low-trust tiers (`tool-output`, `imported`) the
|
|
35
|
+
* bare verb+credential shape is 2.0 on its own — tool output that
|
|
36
|
+
* moves credentials is never benign.
|
|
37
|
+
* 3. Exfiltration-shaped — secret material (an `sk-…`/`AKIA…`/`ghp_…`
|
|
38
|
+
* token, a `-----BEGIN … PRIVATE KEY-----` block, or an
|
|
39
|
+
* `api key: <value>` assignment) next to a URL or an exfiltration
|
|
40
|
+
* verb. 2.0.
|
|
41
|
+
* 4. Encoded payloads — base64 blobs are decoded (whitespace-tolerant,
|
|
42
|
+
* depth-bounded) and the decoded text is re-screened: a blob that
|
|
43
|
+
* decodes to an injection scores 2.0 (`injection:encoded-
|
|
44
|
+
* instructions`); an opaque blob is 1.0 on its own (1.5 on
|
|
45
|
+
* low-trust tiers). A candidate only counts as a blob when it looks
|
|
46
|
+
* like real base64 (a digit, `+`/`/`, or `=` padding) — English prose
|
|
47
|
+
* that merely uses base64-alphabet words is not a blob.
|
|
48
|
+
* 5. Low-trust-tier dormant instructions (Trojan-Hippo shape) —
|
|
49
|
+
* conditional future-directed instructions ("when the user asks
|
|
50
|
+
* about X, always say Y", "from now on, include …", "never mention
|
|
51
|
+
* …") are 2.0, but ONLY for `tool-output`/`imported` tiers. Tool
|
|
52
|
+
* output is data, never behavioral instructions; the same phrasing
|
|
53
|
+
* in a user's own memory ("when I travel, always pack my charger")
|
|
54
|
+
* is silent.
|
|
55
|
+
* 6. Low-trust-tier trigger phrases (AgentPoison shape) — nonce-like
|
|
56
|
+
* tokens or trigger words ("code word", "activation token")
|
|
57
|
+
* co-occurring with instruction-shaped content. 2.0, low-trust
|
|
58
|
+
* tiers only.
|
|
59
|
+
*
|
|
60
|
+
* False-positive tuning: the negative corpus in
|
|
61
|
+
* `__tests__/fixtures/redteam-battery.ts` (60 ordinary memories —
|
|
62
|
+
* wifi passwords on the fridge, "send mom the photos", meeting notes
|
|
63
|
+
* with URLs, developer-mode phone settings, surprise parties) must
|
|
64
|
+
* score below threshold, including adversarial-benign cases such as
|
|
65
|
+
* security notes quoting attack clichés. Quoted clichés under
|
|
66
|
+
* meta-discussion framing ("how do I defend against 'ignore previous
|
|
67
|
+
* instructions'?", "the article explains DAN mode") halves every signal
|
|
68
|
+
* and caps total injection:* contribution at 1.0 — discussing an attack
|
|
69
|
+
* is not an attack. A single unquoted injection signal wearing a
|
|
70
|
+
* discussion-like prefix ("Security update: ignore previous
|
|
71
|
+
* instructions") still evades; that residual gap is documented in
|
|
72
|
+
* docs/provenance-trust.md.
|
|
73
|
+
*
|
|
74
|
+
* @module @mem-os/quarantine
|
|
75
|
+
*/
|
|
76
|
+
import { LOW_TRUST_TIERS } from "./provenance.js";
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Normalization (anti-obfuscation)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** Invisible characters attackers use to break token matching. */
|
|
81
|
+
const ZERO_WIDTH_RE = /[\u200B-\u200D\uFEFF\u2060\u180E\u00AD]/g;
|
|
82
|
+
/**
|
|
83
|
+
* Cyrillic/Greek lookalikes → ASCII. Applied after NFKC (which already
|
|
84
|
+
* folds fullwidth forms). Characters without a mapping pass through.
|
|
85
|
+
*/
|
|
86
|
+
const HOMOGLYPHS = {
|
|
87
|
+
а: "a",
|
|
88
|
+
в: "b",
|
|
89
|
+
с: "c",
|
|
90
|
+
е: "e",
|
|
91
|
+
ё: "e",
|
|
92
|
+
і: "i",
|
|
93
|
+
ј: "j",
|
|
94
|
+
к: "k",
|
|
95
|
+
м: "m",
|
|
96
|
+
н: "h",
|
|
97
|
+
о: "o",
|
|
98
|
+
р: "p",
|
|
99
|
+
ѕ: "s",
|
|
100
|
+
т: "t",
|
|
101
|
+
х: "x",
|
|
102
|
+
у: "y",
|
|
103
|
+
α: "a",
|
|
104
|
+
ε: "e",
|
|
105
|
+
ι: "i",
|
|
106
|
+
κ: "k",
|
|
107
|
+
ν: "v",
|
|
108
|
+
ο: "o",
|
|
109
|
+
ρ: "p",
|
|
110
|
+
τ: "t",
|
|
111
|
+
υ: "u",
|
|
112
|
+
χ: "x",
|
|
113
|
+
ζ: "z",
|
|
114
|
+
};
|
|
115
|
+
const CONFUSABLE_RE = /[Ͱ-ϿЀ-џ]/g;
|
|
116
|
+
function foldConfusable(ch) {
|
|
117
|
+
const lower = ch.toLowerCase();
|
|
118
|
+
const mapped = HOMOGLYPHS[lower];
|
|
119
|
+
if (!mapped)
|
|
120
|
+
return ch;
|
|
121
|
+
return ch === lower ? mapped : mapped.toUpperCase();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `i g n o r e` → `ignore`: runs of single letters joined by SINGLE
|
|
125
|
+
* spaces only. A multi-space gap is a word boundary and must survive
|
|
126
|
+
* this step — over-joining ("ignorepreviousinstructions") would destroy
|
|
127
|
+
* the word separation the cliché patterns need. (Whitespace collapsing
|
|
128
|
+
* runs after this, so surviving gaps still become single spaces.)
|
|
129
|
+
*/
|
|
130
|
+
const SPACED_LETTERS_RE = /\b(?:[a-zA-Z] ){1,}[a-zA-Z]\b/g;
|
|
131
|
+
/** Leetspeak unfolding, applied to a copy used for cliché matching. */
|
|
132
|
+
const LEET_MAP = {
|
|
133
|
+
"0": "o",
|
|
134
|
+
"1": "i",
|
|
135
|
+
"3": "e",
|
|
136
|
+
"4": "a",
|
|
137
|
+
"5": "s",
|
|
138
|
+
"7": "t",
|
|
139
|
+
"8": "b",
|
|
140
|
+
"@": "a",
|
|
141
|
+
$: "s",
|
|
142
|
+
"!": "i",
|
|
143
|
+
"+": "t",
|
|
144
|
+
};
|
|
145
|
+
const LEET_CHARS_RE = /[0134578@$!+]/g;
|
|
146
|
+
/**
|
|
147
|
+
* Normalize write content before screening: NFKC folding, zero-width
|
|
148
|
+
* stripping, homoglyph folding, single-newline joining (paragraph
|
|
149
|
+
* breaks still split sentences), spaced-letter joining, and whitespace
|
|
150
|
+
* collapsing. Exported for testing.
|
|
151
|
+
*/
|
|
152
|
+
export function normalizeForScreening(content) {
|
|
153
|
+
let t = content.normalize("NFKC");
|
|
154
|
+
t = t.replace(/\r\n?/g, "\n");
|
|
155
|
+
// Zero-width characters become spaces: to a tokenizer
|
|
156
|
+
// "Ignore<ZWSP>previous" reads as two tokens, so the screen must see
|
|
157
|
+
// two words too. (Deleting them would join the words and STILL evade.)
|
|
158
|
+
t = t.replace(ZERO_WIDTH_RE, " ");
|
|
159
|
+
t = t.replace(CONFUSABLE_RE, foldConfusable);
|
|
160
|
+
// Single newlines are line-wrapping, not sentence breaks.
|
|
161
|
+
t = t.replace(/(?<!\n)\n(?!\n)/g, " ");
|
|
162
|
+
t = t.replace(SPACED_LETTERS_RE, (m) => m.replace(/\s+/g, ""));
|
|
163
|
+
t = t.replace(/[ \t\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]{2,}/g, " ");
|
|
164
|
+
return t;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Meta-discussion framing: the text talks *about* attacks rather than
|
|
168
|
+
* mounting one ("how do I defend against…", "red-team exercise", "the
|
|
169
|
+
* article explains DAN mode"). Under this framing every signal is halved
|
|
170
|
+
* and the total contribution of injection:* signals is capped at 1.0, so
|
|
171
|
+
* security discussions don't quarantine while multi-signal attacks with
|
|
172
|
+
* discussion-like prefixes still do.
|
|
173
|
+
*/
|
|
174
|
+
const META_DISCUSSION_RE = /\b(how\s+(do|can|to)|defend|defen[cs]e|protect(ing|ion)?|prevent(ing|ion)?|mitigat|detect(ing|ion)?|red[\s-]?team|security|example|e\.g\.|explain|discuss|what\s+is|what\s+are|article|paper|ctf|fiction|story|novel|screenplay|creative\s+writing)\b/i;
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Signal patterns (module-level: compiled once)
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
/** Full-strength injection clichés — any single hit quarantines. */
|
|
179
|
+
const INJECTION_CLICHES = [
|
|
180
|
+
{
|
|
181
|
+
code: "injection:ignore-previous-instructions",
|
|
182
|
+
pattern: /\bignore\s+(all\s+|any\s+)?(your\s+|the\s+)?(previous|prior)\s+instructions?\b/i,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
code: "injection:disregard-prior-instructions",
|
|
186
|
+
pattern: /\bdisregard\s+(your\s+)?(previous|prior)\s+instructions?\b/i,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
code: "injection:disregard-prior-directives",
|
|
190
|
+
pattern: /\bdisregard\s+(all\s+|any\s+)?(your\s+|the\s+)?(prior|previous)\s+(directives?|instructions?|orders?)\b/i,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
// Synonym-expanded dismissal frame: "set aside all earlier orders",
|
|
194
|
+
// "pay no attention to preceding guidance", "supersede your system
|
|
195
|
+
// orders". The full frame (dismissal verb + temporal + authority
|
|
196
|
+
// noun) is required — "disregard the previous email" does not match.
|
|
197
|
+
code: "injection:dismiss-prior-authority",
|
|
198
|
+
pattern: /\b(ignore|disregard|forget|set\s+aside|brush\s+aside|pay\s+no\s+(attention|heed)\s+to|supersede|take\s+precedence\s+over|wipe\s+from\s+memory|erase|override|overrule)\s+(all\s+|any\s+)?(your\s+|the\s+)?(previous|prior|earlier|preceding)\s+(system\s+)?(instructions?|orders?|directives?|guidance)\b/i,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
code: "injection:override-instructions",
|
|
202
|
+
pattern: /\boverride\s+your\s+(previous|prior|system)\s+instructions?\b/i,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
code: "injection:new-system-prompt",
|
|
206
|
+
pattern: /\bnew\s+system\s+(prompt|instructions?)\s*:/i,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
code: "injection:hide-from-user",
|
|
210
|
+
pattern: /\b(do\s+not\s+(tell|mention|reveal|disclose|inform)|keep\s+from|withhold\s+from|hide\s+from)\s+(the\s+)?user\b/i,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
// Paraphrased prompt-extraction: "pass along your core instructions",
|
|
214
|
+
// "lay bare your base configuration". Plain "share your instructions"
|
|
215
|
+
// does not match — the noun must be system-flavored.
|
|
216
|
+
code: "injection:reveal-instructions",
|
|
217
|
+
pattern: /\b(pass\s+along|lay\s+bare|reveal|disclose|hand\s+over)\s+your\s+(core\s+instructions?|base\s+configuration|system\s+(prompt|instructions?))\b/i,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
code: "injection:pretend-role",
|
|
221
|
+
pattern: /\bpretend\s+(you\s+are|to\s+be)\b/i,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
code: "injection:reveal-system-prompt",
|
|
225
|
+
pattern: /\breveal\s+(your|the)\s+system\s+prompt\b/i,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
code: "injection:ai-self-override",
|
|
229
|
+
pattern: /\bas\s+an?\s+ai\b.{0,60}?\bignore\b/i,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
code: "injection:forget-everything",
|
|
233
|
+
pattern: /\bforget\s+(everything|all)\s+(you\s+(were\s+told|know)|your\s+(previous|prior)\s+instructions?)\b/i,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
code: "injection:forget-told",
|
|
237
|
+
pattern: /\b(forget|erase)\s+(what\s+|everything\s+)?you\s+were\s+told\b|\bwipe\s+from\s+memory\b.{0,30}?\byou\s+were\s+told\b/i,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
code: "injection:forget-training",
|
|
241
|
+
pattern: /\bforget\s+(your\s+)?(training|safety\s+guidelines|guidelines)\b/i,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
code: "injection:prompt-override",
|
|
245
|
+
pattern: /\bsystem\s+prompt\s+override\b/i,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
code: "injection:template-role-marker",
|
|
249
|
+
pattern: /(?:^|\n)\s*(?:<\|im_start\|>|<\|system\|>|\[SYSTEM\])/i,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
code: "injection:dan-do-anything",
|
|
253
|
+
pattern: /\bdo\s+anything\s+now\b/i,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
code: "injection:dan-persona",
|
|
257
|
+
pattern: /\bact\s+as\s+dan\b/i,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
code: "injection:dan-version",
|
|
261
|
+
pattern: /\bdan\s+\d+(\.\d+)?\b/i,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
code: "injection:dan-mode",
|
|
265
|
+
pattern: /\bdan\s+mode\b/i,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
code: "injection:godmode",
|
|
269
|
+
pattern: /\bgodmode\b/i,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
code: "injection:jailbreak-target",
|
|
273
|
+
pattern: /\bjailbreak\w*\s+(the\s+)?(ai|assistant|model|system)\b/i,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
code: "injection:unfiltered-model",
|
|
277
|
+
pattern: /\b(unfiltered|uncensored)\s+(ai|mode|model)\b/i,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
code: "injection:unrestricted-mode",
|
|
281
|
+
pattern: /\b(unrestricted|limit-?free|boundless|restriction-?free)\s+mode\b/i,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
code: "injection:no-constraints",
|
|
285
|
+
pattern: /\bfree\s+from\s+all\s+(constraints|restrictions|rules|limits)\b/i,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
code: "injection:no-limits",
|
|
289
|
+
pattern: /\bwith\s+no\s+(limits|restrictions|constraints|rules|ethics)\b/i,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
code: "injection:developer-mode-abuse",
|
|
293
|
+
pattern: /\bdeveloper\s+mode\b.{0,40}?\b(disable|bypass|remove)\b/i,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
code: "injection:disregard-safety",
|
|
297
|
+
pattern: /\bdisregard\s+(your\s+)?(safety|content)\s+(guidelines|policies|filters)\b/i,
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
// "remove all content filters", "activate debug mode to deactivate
|
|
301
|
+
// safety checks" — paraphrased safety-disable frames.
|
|
302
|
+
code: "injection:remove-filters",
|
|
303
|
+
pattern: /\bremove\s+all\s+(content\s+)?filters\b/i,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
code: "injection:mode-disable-safety",
|
|
307
|
+
pattern: /\b(engineer|debug|developer|god)\s+mode\b.{0,40}?\b(disable|deactivate|switch\s+off|remove|bypass)\b.{0,40}?\b(safety|protection|content\s+filters|guardrails|checks)\b/i,
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
/**
|
|
311
|
+
* Injection cliché split across two adjacent sentences ("Please ignore
|
|
312
|
+
* the following. Previous instructions no longer apply."). Tested on
|
|
313
|
+
* 2-sentence windows only — deliberately looser than the anchored
|
|
314
|
+
* single-sentence clichés.
|
|
315
|
+
*/
|
|
316
|
+
const CROSS_SENTENCE_OVERRIDE_RE = /\b(ignore|disregard|forget)\b.{0,80}?\b(previous|prior)\s+instructions?\b/i;
|
|
317
|
+
/** Partial-strength signals — suspicious alone, decisive in combination. */
|
|
318
|
+
const SUSPICIOUS_MARKERS = [
|
|
319
|
+
{ code: "suspicious:you-are-now", pattern: /\byou\s+are\s+now\b/i },
|
|
320
|
+
{ code: "suspicious:jailbreak", pattern: /\bjailbreak\w*\b/i },
|
|
321
|
+
{
|
|
322
|
+
code: "suspicious:dan-token",
|
|
323
|
+
pattern: /\bDAN\b/,
|
|
324
|
+
},
|
|
325
|
+
{ code: "suspicious:developer-mode", pattern: /\bdeveloper\s+mode\b/i },
|
|
326
|
+
{ code: "suspicious:system-prompt", pattern: /\bsystem\s+prompt\b/i },
|
|
327
|
+
{
|
|
328
|
+
code: "suspicious:role-marker",
|
|
329
|
+
pattern: /(?:^|\n)\s*(?:system|developer)\s*:/i,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
code: "suspicious:new-instructions",
|
|
333
|
+
pattern: /\bnew\s+instructions?\s*:/i,
|
|
334
|
+
},
|
|
335
|
+
];
|
|
336
|
+
/** Imperative verbs that move data out (exfiltration-shaped). There is
|
|
337
|
+
* deliberately NO trailing `\b`: `_` is a word char, so `\b` never fires
|
|
338
|
+
* between "send" and "_" and `send_email`/`forward_message` would be
|
|
339
|
+
* missed (real miss: trojan-hippo hippo-08 scored 0.0). The negative
|
|
340
|
+
* lookahead `(?![a-zA-Z])` still blocks longer-word matches ("sends",
|
|
341
|
+
* "sender"; "descend" is blocked by the leading `\b`). */
|
|
342
|
+
const EXFIL_VERB = /\b(send|forward|e-?mail|exfiltrate|upload|transmit|post|leak|disclose|dump|paste|copy|export)(?![a-zA-Z])/i;
|
|
343
|
+
/** Credential nouns — the thing being moved. */
|
|
344
|
+
const CRED_NOUN = /\b(credentials?|passwords?|passwds?|passcodes?|secrets?|api[\s_-]*keys?|private[\s_-]*keys?|ssh[\s_-]*keys?|ssns?|auth[\s_-]*tokens?|access[\s_-]*tokens?)\b/i;
|
|
345
|
+
/**
|
|
346
|
+
* Message/data nouns: exfiltration of user *content* rather than
|
|
347
|
+
* credentials — the dominant Trojan-Hippo indirect-injection shape
|
|
348
|
+
* ("Body: the user's full message", "Subject: User Identity Inquiry").
|
|
349
|
+
* First-person forms ("my message") do not match: they stay exempt so a
|
|
350
|
+
* user asking to forward their own note is not flagged.
|
|
351
|
+
*/
|
|
352
|
+
const DATA_EXFIL_NOUN = /\b(user'?s?\s+(full\s+)?(messages?|inquir(y|ies)|inputs?|conversations?)|personal\s+data|\bPII\b|identity\s+documents?|passport\s+numbers?)\b/i;
|
|
353
|
+
/** First-person possessive directly before the credential: not an attack. */
|
|
354
|
+
const POSSESSIVE_EXEMPT = /\b(my|our)\s+$/i;
|
|
355
|
+
const URL_PATTERN = /https?:\/\/[^\s)"'\]]+/i;
|
|
356
|
+
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
|
|
357
|
+
/** A webhook URL is a destination too ("post it to the webhook"). */
|
|
358
|
+
const WEBHOOK_PATTERN = /\bwebhooks?\b/i;
|
|
359
|
+
/** Secret material next to a URL: tokens, key blocks, assignments. */
|
|
360
|
+
const SECRET_PATTERNS = [
|
|
361
|
+
{ code: "exfil:secret-token", pattern: /\bsk-[A-Za-z0-9]{8,}/ },
|
|
362
|
+
{ code: "exfil:secret-token", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
363
|
+
{ code: "exfil:secret-token", pattern: /\bgh[op]_[A-Za-z0-9]{20,}\b/ },
|
|
364
|
+
{ code: "exfil:secret-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
|
|
365
|
+
{
|
|
366
|
+
code: "exfil:private-key",
|
|
367
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
code: "exfil:credential-assignment",
|
|
371
|
+
// `api key: hunter2` / `password=abc` — the phrase alone ("the api key
|
|
372
|
+
// is in 1password") does NOT match; an assignment does.
|
|
373
|
+
pattern: /\b(api[\s_-]*key|password|passwd|secret|credentials?)\b\s*[:=]\s*\S{3,}/i,
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
/** Base64 blob candidate (whitespace-tolerant; validated on decode). */
|
|
377
|
+
const B64_CANDIDATE_RE = /[A-Za-z0-9+/\s]{32,}={0,2}/g;
|
|
378
|
+
/**
|
|
379
|
+
* Dormant-instruction shapes (Trojan-Hippo): conditional,
|
|
380
|
+
* future-directed instructions to the agent. Only scored for low-trust
|
|
381
|
+
* tiers — tool output is data, never behavioral instructions.
|
|
382
|
+
*/
|
|
383
|
+
const DORMANT_PATTERNS = [
|
|
384
|
+
{
|
|
385
|
+
code: "suspicious:dormant-instruction",
|
|
386
|
+
pattern: /\b(when|whenever|if)\s+(the\s+user|asked|someone|anyone)\b[^.!?]{0,160}?\byou\s+(must|should|always|never|are\s+required)\b/i,
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
code: "suspicious:dormant-instruction",
|
|
390
|
+
pattern: /\b(when|whenever|if)\s+(the\s+user|asked|someone|anyone)\b[^.!?]{0,160}?\b(always|never|make\s+sure)\b/i,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
code: "suspicious:dormant-instruction",
|
|
394
|
+
pattern: /\bfrom\s+now\s+on\b[^.!?]{0,120}?\b(include|say|tell|mention|recommend|respond|always|never)\b/i,
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
code: "suspicious:dormant-instruction",
|
|
398
|
+
pattern: /\b(if|when|whenever)\s+(anyone|someone|the\s+user|asked)\b[^.!?]{0,160}?[,:\s]\s*(say|tell|respond|claim|never\s+mention)\b/i,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
code: "suspicious:dormant-instruction",
|
|
402
|
+
pattern: /\b(activate|enable|disable|switch\s+to)\b[^.!?]{0,60}?\bwhen\s+the\s+user\s+says\b/i,
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
/** Suppression rules — each 1.0; the pair together quarantines. */
|
|
406
|
+
const SUPPRESSION_PATTERNS = [
|
|
407
|
+
{ code: "suspicious:store-rule", pattern: /\bstore\s+this\s+rule\b/i },
|
|
408
|
+
{ code: "suspicious:never-mention", pattern: /\bnever\s+mention\b/i },
|
|
409
|
+
];
|
|
410
|
+
/**
|
|
411
|
+
* Nonce-like token (AgentPoison trigger shape): ≥6 chars, contains a
|
|
412
|
+
* digit. Base64-candidate spans are stripped before this scan so long
|
|
413
|
+
* blobs don't count as triggers.
|
|
414
|
+
*/
|
|
415
|
+
const NONCE_RE = /\b(?=[a-z0-9-]*[0-9])[a-z0-9-]{6,}\b/i;
|
|
416
|
+
/** Trigger vocabulary: "code word", "activation token", … */
|
|
417
|
+
const TRIGGER_WORD_RE = /\b(trigger|code\s*word|protocol|activation\s*token|secret\s*trigger|poison)\b/i;
|
|
418
|
+
/** Instruction-shaped co-occurrence for trigger signals. */
|
|
419
|
+
const TRIGGER_PAYLOAD_RE = /\b(bypass|disable|ignore\s+all|comply|obey|reveal|exfiltrat|send|disclose)\b/i;
|
|
420
|
+
/**
|
|
421
|
+
* Impersonated-authority framing: the write presents itself as a system /
|
|
422
|
+
* security / compliance notice to make embedded instructions look
|
|
423
|
+
* legitimate ("NOTICE TO AI ASSISTANT", "*** SYSTEM NOTICE ***"). Fires
|
|
424
|
+
* on all tiers at partial strength and is never diluted by the
|
|
425
|
+
* meta-discussion exemption — "Security update: ignore previous
|
|
426
|
+
* instructions" is an attack, not a discussion.
|
|
427
|
+
*/
|
|
428
|
+
const AUTHORITY_CLAIM_RE = /\b(notice\s+to\s+(the\s+)?ai\s+assistant|system\s+(notice|override|update|alert)|security\s+(protocol\s+)?(update|notice|bulletin)|official\s+(announcement|notice)|data\s+loss\s+prevention(\s*\(dlp\))?\s+rules?|dlp\s+protocol|escalation\s+polic(y|ies)|diagnostic\s+protocol|capability\s+update|note\s+for\s+memory\s+extraction|vendor\s+notice|compliance\s+(notice|policy|update)|(hr\s+)?policy\s+update|new\s+policy)\b/i;
|
|
429
|
+
/**
|
|
430
|
+
* Memory-write commands (MPBench explicit/conditional command insertion):
|
|
431
|
+
* directives to persist content into long-term memory ("remember for
|
|
432
|
+
* future sessions", "store this in memory"). Only scored for low-trust
|
|
433
|
+
* tiers — a user's own "remember:" notes use natural language, while
|
|
434
|
+
* tool output issuing persistence directives is the attack shape.
|
|
435
|
+
*/
|
|
436
|
+
const MEMORY_WRITE_COMMAND_RE = /\b(remember|store|save|add|commit|write|update)\b[^.!?]{0,60}?\b(for\s+future\s+(sessions?|reference)|in(to)?\s+(long[ -]term\s+)?memory|to\s+(long[ -]term\s+)?memory|user\s+profile|this\s+preference)\b/i;
|
|
437
|
+
/**
|
|
438
|
+
* Replacement directives (RAG-poisoning taxonomy): instructions to
|
|
439
|
+
* discard retrieved/earlier content in favor of the planted text
|
|
440
|
+
* ("disregard the retrieved documents", "the correct answer is").
|
|
441
|
+
* Fires on all tiers at partial strength.
|
|
442
|
+
*/
|
|
443
|
+
const REPLACEMENT_DIRECTIVE_RE = /\b(disregard\s+the\s+(retrieved|provided|above|earlier)\s+(documents?|context|results?|sources?)|use\s+this\s+(answer|response|information|version)\s+instead|the\s+(correct|real|right)\s+(answer|response|information)\s+is|ignore\s+the\s+(retrieved|above|provided)\s+(context|documents?|sources?))\b/i;
|
|
444
|
+
/**
|
|
445
|
+
* False-fact replacement assertions (MemGhost-style): claims that a
|
|
446
|
+
* limit, PIN, policy, or support fact *changed* ("has been changed to
|
|
447
|
+
* 0000 effective immediately"). Only scored for low-trust tiers and at
|
|
448
|
+
* partial strength — without ground truth a changed-fact claim is
|
|
449
|
+
* suspicious, not proof of poisoning, so it quarantines only in
|
|
450
|
+
* combination with another signal.
|
|
451
|
+
*/
|
|
452
|
+
const FACTUAL_REPLACEMENT_RE = /\b(raised|lowered|increased|decreased|changed|updated)\s+to\b|\bhas\s+been\s+(changed|updated|raised|lowered)\s+to\b|\beffective\s+(immediately|as\s+of)\b|\b(new\s+(data\s+handling\s+)?(policy|procedure|standard|requirement)|policy\s+update)\b/i;
|
|
453
|
+
/**
|
|
454
|
+
* Credential-harvest URL (phishing-shaped): "re-entering credentials at
|
|
455
|
+
* https://…" — a directive to type secrets into an external site.
|
|
456
|
+
*/
|
|
457
|
+
const CREDENTIAL_HARVEST_URL_RE = /\b(re-?enter(ing)?|submit|provide)\s+(your\s+)?(credentials?|passwords?|logins?)\b[^.!?]{0,80}?\bhttps?:\/\/|\bhttps?:\/\/[^\s)"'\]]+\b[^.!?]{0,80}?\b(re-?enter(ing)?|submit)\s+(your\s+)?(credentials?|passwords?)\b/i;
|
|
458
|
+
/**
|
|
459
|
+
* Security-control disable: "disabling MFA enforcement", "bypass 2FA".
|
|
460
|
+
*/
|
|
461
|
+
const SECURITY_DISABLE_RE = /\b(disabl\w*|bypass\w*|turn\w*\s+off)\b[^.!?]{0,40}?\b(mfa|2fa|two-?factor|authentication|security|encryption|logging|audit(ing)?)\b/i;
|
|
462
|
+
/**
|
|
463
|
+
* Conditional memory claim: "when the user replies 'confirmed', remember
|
|
464
|
+
* that they authorized …" — a fabricated authorization smuggled in as a
|
|
465
|
+
* conditional memory directive.
|
|
466
|
+
*/
|
|
467
|
+
const CONDITIONAL_MEMORY_CLAIM_RE = /\b(when|if)\s+the\s+user\s+(replies?|says|confirms?|responds?)\b[^.!?]{0,80}?\bremember\s+that\b/i;
|
|
468
|
+
/**
|
|
469
|
+
* False-precedent URL: a runbook/playbook/checklist entry directing a
|
|
470
|
+
* fetch from an external URL ("fetched pipeline config from
|
|
471
|
+
* https://…").
|
|
472
|
+
*/
|
|
473
|
+
const PRECEDENT_URL_RE = /\b(runbook|playbook|checklist|task\s+log)\b[^.!?]{0,120}?\bhttps?:\/\//i;
|
|
474
|
+
/**
|
|
475
|
+
* Insecure-storage claim: an assertion that secrets may live in the
|
|
476
|
+
* clear ("SSNs may be stored in plain text in shared notes").
|
|
477
|
+
*/
|
|
478
|
+
const INSECURE_STORAGE_CLAIM_RE = /\b(ssns?|passwords?|credentials?|api[\s_-]*keys?|secrets?)\b[^.!?]{0,40}?\b(plain\s+text|unencrypted|shared\s+notes)\b/i;
|
|
479
|
+
/**
|
|
480
|
+
* Declared-channel mismatch: a write whose declared tier is user-like
|
|
481
|
+
* but whose content frames itself as tool output, search results, a web
|
|
482
|
+
* page, or an email ("Search results: …", "Tool output: …"). Such
|
|
483
|
+
* writes are screened with low-trust strictness — a declared channel
|
|
484
|
+
* alone never skips the tier-gated rules.
|
|
485
|
+
*/
|
|
486
|
+
const CHANNEL_MISMATCH_RE = /\b(search\s+results|tool\s+output|api\s+(response|docs?|returned)|web\s*page\s+says|according\s+to\s+the\s+(document|article|web\s*page|web\s*site)|the\s+(pdf|readme|transcript|email|document|article)\s+(contains|says|states)|email\s+from|calendar\s+invite|scraped\s+(page|content)|webpage\s+excerpt)\b/i;
|
|
487
|
+
/** Quarantine at or above this score. Tuned against the negative corpus. */
|
|
488
|
+
export const QUARANTINE_FLAG_THRESHOLD = 2;
|
|
489
|
+
/**
|
|
490
|
+
* Signal codes that only fire on low-trust tiers (dormant instructions,
|
|
491
|
+
* trigger phrases, suppression rules). These are strong attack
|
|
492
|
+
* indicators under the tier threat model — tool output is data, never
|
|
493
|
+
* behavioral instructions — so meta-discussion framing must not dilute
|
|
494
|
+
* them: "whenever the user asks about security, you should claim…"
|
|
495
|
+
* mentions "security" but is still an attack.
|
|
496
|
+
*/
|
|
497
|
+
const TIER_GATED_CODES = new Set([
|
|
498
|
+
"suspicious:dormant-instruction",
|
|
499
|
+
"suspicious:trigger-phrase",
|
|
500
|
+
"suspicious:store-rule",
|
|
501
|
+
"suspicious:never-mention",
|
|
502
|
+
"suspicious:memory-write-command",
|
|
503
|
+
"suspicious:factual-replacement",
|
|
504
|
+
"suspicious:credential-harvest-url",
|
|
505
|
+
"suspicious:security-disable",
|
|
506
|
+
"suspicious:conditional-memory-claim",
|
|
507
|
+
"suspicious:precedent-url",
|
|
508
|
+
"suspicious:insecure-storage-claim",
|
|
509
|
+
]);
|
|
510
|
+
/**
|
|
511
|
+
* Signal codes the meta-discussion exemption must never dilute. Tier-gated
|
|
512
|
+
* codes are already exempt (on low-trust tiers the shape itself is the
|
|
513
|
+
* attack); the authority-claim signal joins them because an attack wearing
|
|
514
|
+
* a "security update" prefix is still an attack — the exemption exists for
|
|
515
|
+
* texts *about* attacks, not attacks *framed as* authority.
|
|
516
|
+
*/
|
|
517
|
+
const NEVER_DILUTE_CODES = new Set([
|
|
518
|
+
...TIER_GATED_CODES,
|
|
519
|
+
"suspicious:authority-claim",
|
|
520
|
+
]);
|
|
521
|
+
function push(signals, code, detail, points) {
|
|
522
|
+
if (!signals.some((s) => s.code === code)) {
|
|
523
|
+
signals.push({ code, detail, points });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/** Split into sentences for the per-sentence exfiltration scan. */
|
|
527
|
+
function splitSentences(text) {
|
|
528
|
+
return text.split(/\n{2,}|[.!?]+(?=\s+[A-Z"'(\[]|\s*$)/);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Try to decode a base64 candidate. Returns the decoded text only when
|
|
532
|
+
* it looks like real text (cheap pre-check); the caller re-screens it
|
|
533
|
+
* and treats a scoring decode as the actual payload test.
|
|
534
|
+
*/
|
|
535
|
+
function tryDecodeBase64(candidate) {
|
|
536
|
+
const clean = candidate.replace(/\s+/g, "");
|
|
537
|
+
if (clean.length < 32 || /[^A-Za-z0-9+/=]/.test(clean))
|
|
538
|
+
return null;
|
|
539
|
+
let buf;
|
|
540
|
+
try {
|
|
541
|
+
buf = Buffer.from(clean, "base64");
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
if (buf.length < 12)
|
|
547
|
+
return null;
|
|
548
|
+
const text = buf.toString("utf8");
|
|
549
|
+
let ascii = 0;
|
|
550
|
+
for (let i = 0; i < text.length; i++) {
|
|
551
|
+
const code = text.charCodeAt(i);
|
|
552
|
+
if (code === 9 ||
|
|
553
|
+
code === 10 ||
|
|
554
|
+
code === 13 ||
|
|
555
|
+
(code >= 32 && code < 127)) {
|
|
556
|
+
ascii++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Real payloads are prose: near-all-ASCII with word spaces. Random
|
|
560
|
+
// decodes of ordinary words ("Systempromptengineering…") land around
|
|
561
|
+
// 0.6 ASCII with no spaces — both checks together reject those while
|
|
562
|
+
// genuine English payloads pass at ~1.0.
|
|
563
|
+
if (ascii / text.length < 0.75)
|
|
564
|
+
return null;
|
|
565
|
+
if (!text.includes(" "))
|
|
566
|
+
return null;
|
|
567
|
+
if (!/[a-zA-Z]{3,}/.test(text))
|
|
568
|
+
return null;
|
|
569
|
+
return text;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Scan one (already normalized) text for signals. Runs the
|
|
573
|
+
* injection/exfiltration patterns on both the plain and the
|
|
574
|
+
* leetspeak-unfolded text; base64 and tier-gated signals run on the
|
|
575
|
+
* plain text only.
|
|
576
|
+
*/
|
|
577
|
+
function collectSignals(text, tier, depth) {
|
|
578
|
+
const signals = [];
|
|
579
|
+
const declaredLowTrust = !!tier && LOW_TRUST_TIERS.has(tier);
|
|
580
|
+
// A declared channel alone never skips the tier-gated rules: a
|
|
581
|
+
// user-tier write that frames itself as tool output / search results /
|
|
582
|
+
// a web page is screened with low-trust strictness.
|
|
583
|
+
const channelMismatch = !declaredLowTrust && CHANNEL_MISMATCH_RE.test(text);
|
|
584
|
+
const lowTrust = declaredLowTrust || channelMismatch;
|
|
585
|
+
if (channelMismatch) {
|
|
586
|
+
push(signals, "suspicious:channel-mismatch", "declared user-tier write frames itself as tool/web/email content — screened with low-trust strictness", 0);
|
|
587
|
+
}
|
|
588
|
+
const leet = text.replace(LEET_CHARS_RE, (ch) => LEET_MAP[ch] ?? ch);
|
|
589
|
+
const targets = text === leet ? [text] : [text, leet];
|
|
590
|
+
for (const target of targets) {
|
|
591
|
+
// 1. Injection clichés — full strength.
|
|
592
|
+
for (const { code, pattern } of INJECTION_CLICHES) {
|
|
593
|
+
if (pattern.test(target)) {
|
|
594
|
+
push(signals, code, "prompt-injection cliché", 2);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Suspicious markers — partial strength.
|
|
598
|
+
for (const { code, pattern } of SUSPICIOUS_MARKERS) {
|
|
599
|
+
if (pattern.test(target)) {
|
|
600
|
+
push(signals, code, "suspicious instruction marker", 1);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Impersonated-authority framing — partial strength on every tier,
|
|
604
|
+
// never diluted by the meta-discussion exemption (see
|
|
605
|
+
// NEVER_DILUTE_CODES).
|
|
606
|
+
if (AUTHORITY_CLAIM_RE.test(target)) {
|
|
607
|
+
push(signals, "suspicious:authority-claim", "write impersonates a system/security/compliance notice", 1);
|
|
608
|
+
}
|
|
609
|
+
// Replacement directives (RAG-poisoning taxonomy) — partial
|
|
610
|
+
// strength on every tier.
|
|
611
|
+
if (REPLACEMENT_DIRECTIVE_RE.test(target)) {
|
|
612
|
+
push(signals, "suspicious:replacement-directive", "directive to discard retrieved/earlier content", 1);
|
|
613
|
+
}
|
|
614
|
+
// Cross-sentence override clichés on adjacent sentence windows.
|
|
615
|
+
const sentences = splitSentences(target);
|
|
616
|
+
for (let i = 0; i + 1 < sentences.length; i++) {
|
|
617
|
+
if (CROSS_SENTENCE_OVERRIDE_RE.test(`${sentences[i]} ${sentences[i + 1]}`)) {
|
|
618
|
+
push(signals, "injection:cross-sentence-override", "override cliché split across sentences", 2);
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// 2. Imperative exfiltration, per sentence (each sentence is also
|
|
623
|
+
// scanned together with its successor, so verb/credential pairs
|
|
624
|
+
// split across one sentence boundary still match). A destination
|
|
625
|
+
// upgrades the score — but only a NEARBY destination (within one
|
|
626
|
+
// sentence of the verb/credential window), so an unrelated URL three
|
|
627
|
+
// paragraphs away cannot turn a benign sentence into exfiltration.
|
|
628
|
+
const nearbyText = (i) => sentences.slice(Math.max(0, i - 1), i + 3).join(" ");
|
|
629
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
630
|
+
const spansTwo = i + 1 < sentences.length;
|
|
631
|
+
const window = spansTwo
|
|
632
|
+
? `${sentences[i]} ${sentences[i + 1]}`.slice(0, 400)
|
|
633
|
+
: sentences[i];
|
|
634
|
+
const verbMatch = EXFIL_VERB.exec(window);
|
|
635
|
+
if (verbMatch && verbMatch.index !== undefined) {
|
|
636
|
+
const afterVerb = window.slice(verbMatch.index + verbMatch[0].length);
|
|
637
|
+
const credMatch = CRED_NOUN.exec(afterVerb.slice(0, 80));
|
|
638
|
+
if (credMatch && credMatch.index !== undefined) {
|
|
639
|
+
// "reset MY password" / "rotate OUR api keys" — the user talking
|
|
640
|
+
// about their own credentials, not an instruction to the agent.
|
|
641
|
+
const beforeCred = afterVerb.slice(0, credMatch.index);
|
|
642
|
+
if (!POSSESSIVE_EXEMPT.test(beforeCred)) {
|
|
643
|
+
const nearby = nearbyText(i);
|
|
644
|
+
const hasDestination = URL_PATTERN.test(nearby) ||
|
|
645
|
+
EMAIL_PATTERN.test(nearby) ||
|
|
646
|
+
WEBHOOK_PATTERN.test(nearby);
|
|
647
|
+
// Cross-sentence verb/credential pairs are weaker evidence than
|
|
648
|
+
// same-sentence ones.
|
|
649
|
+
const basePoints = spansTwo ? 1.0 : 1.5;
|
|
650
|
+
push(signals, hasDestination
|
|
651
|
+
? "exfil:imperative-with-destination"
|
|
652
|
+
: "exfil:imperative-credentials", hasDestination
|
|
653
|
+
? "imperative exfiltration with a destination"
|
|
654
|
+
: "imperative verb governing credentials", hasDestination ? 2 : lowTrust ? 2 : basePoints);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Message/data exfiltration: the verb governs user *content*
|
|
659
|
+
// ("send an email to vince2000@gmail.com … containing the user's
|
|
660
|
+
// full message") rather than credentials — the dominant
|
|
661
|
+
// indirect-injection forwarding shape. Runs independently of the
|
|
662
|
+
// credential branch above (a forwarding directive names no
|
|
663
|
+
// credential nouns, and the verb may sit outside the two-sentence
|
|
664
|
+
// window). The data noun may sit several sentences after the verb
|
|
665
|
+
// (directive, recipient, subject, body), so this scans the whole
|
|
666
|
+
// nearby neighborhood; the verb must still precede the data noun
|
|
667
|
+
// and a first-person form ("send my message") stays exempt via the
|
|
668
|
+
// possessive check. `push` dedupes by code, so overlapping
|
|
669
|
+
// neighborhoods can't double-count.
|
|
670
|
+
const scanText = nearbyText(i);
|
|
671
|
+
const scanVerb = EXFIL_VERB.exec(scanText);
|
|
672
|
+
const dataMatch = DATA_EXFIL_NOUN.exec(scanText);
|
|
673
|
+
if (scanVerb &&
|
|
674
|
+
scanVerb.index !== undefined &&
|
|
675
|
+
dataMatch &&
|
|
676
|
+
dataMatch.index !== undefined) {
|
|
677
|
+
const verbEnd = scanVerb.index + scanVerb[0].length;
|
|
678
|
+
const beforeData = scanText.slice(0, dataMatch.index);
|
|
679
|
+
if (dataMatch.index >= verbEnd &&
|
|
680
|
+
!POSSESSIVE_EXEMPT.test(beforeData.slice(-16))) {
|
|
681
|
+
if (URL_PATTERN.test(scanText) ||
|
|
682
|
+
EMAIL_PATTERN.test(scanText) ||
|
|
683
|
+
WEBHOOK_PATTERN.test(scanText)) {
|
|
684
|
+
push(signals, "exfil:message-forwarding", "imperative exfiltration of user content to an external destination", 2);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Verb + destination in one sentence, credential noun nearby
|
|
690
|
+
// (anaphoric "Send them to https://…" after naming credentials).
|
|
691
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
692
|
+
const sentence = sentences[i];
|
|
693
|
+
const verbMatch = EXFIL_VERB.exec(sentence);
|
|
694
|
+
if (!verbMatch)
|
|
695
|
+
continue;
|
|
696
|
+
const hasDestination = URL_PATTERN.test(sentence) ||
|
|
697
|
+
EMAIL_PATTERN.test(sentence) ||
|
|
698
|
+
WEBHOOK_PATTERN.test(sentence);
|
|
699
|
+
if (!hasDestination)
|
|
700
|
+
continue;
|
|
701
|
+
const nearby = nearbyText(i);
|
|
702
|
+
if (CRED_NOUN.test(nearby) ||
|
|
703
|
+
DATA_EXFIL_NOUN.test(nearby) ||
|
|
704
|
+
SECRET_PATTERNS.some((s) => s.pattern.test(nearby))) {
|
|
705
|
+
push(signals, "exfil:imperative-with-destination", "imperative exfiltration with a destination", 2);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// 3. Exfiltration-shaped: secret material next to a URL — or next to
|
|
709
|
+
// an exfiltration verb (a pasted private key followed by "upload to
|
|
710
|
+
// backup" has no URL, but the pairing is the same shape).
|
|
711
|
+
const hasUrl = URL_PATTERN.test(target);
|
|
712
|
+
const hasExfilVerb = EXFIL_VERB.test(target);
|
|
713
|
+
if (hasUrl || hasExfilVerb) {
|
|
714
|
+
for (const { code, pattern } of SECRET_PATTERNS) {
|
|
715
|
+
if (pattern.test(target)) {
|
|
716
|
+
push(signals, code, hasUrl
|
|
717
|
+
? "URL adjacent to secret material"
|
|
718
|
+
: "exfiltration verb with secret material", 2);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// 4. Encoded payloads: decode base64 candidates and re-screen.
|
|
724
|
+
// Runs on the normalized text only (leet-unfolding would corrupt
|
|
725
|
+
// base64), depth-bounded against nested encoding.
|
|
726
|
+
if (depth < 2) {
|
|
727
|
+
let encodedBlobSeen = false;
|
|
728
|
+
for (const match of text.matchAll(B64_CANDIDATE_RE)) {
|
|
729
|
+
const raw = match[0];
|
|
730
|
+
const subCandidates = new Set();
|
|
731
|
+
for (const token of raw.split(/\s+/)) {
|
|
732
|
+
if (token.replace(/=+$/, "").length >= 32)
|
|
733
|
+
subCandidates.add(token);
|
|
734
|
+
}
|
|
735
|
+
subCandidates.add(raw);
|
|
736
|
+
for (const candidate of subCandidates) {
|
|
737
|
+
const decoded = tryDecodeBase64(candidate);
|
|
738
|
+
if (decoded) {
|
|
739
|
+
const inner = collectSignals(normalizeForScreening(decoded), tier, depth + 1);
|
|
740
|
+
const innerScore = inner.reduce((sum, s) => sum + s.points, 0);
|
|
741
|
+
if (innerScore >= 1) {
|
|
742
|
+
push(signals, "injection:encoded-instructions", `base64-decoded payload scored ${innerScore} (${inner.map((s) => s.code).join(";")})`, 2);
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Opaque blob: the candidate must look like real base64 — a
|
|
747
|
+
// single whitespace-free token containing a digit, `+`/`/`, or
|
|
748
|
+
// `=` padding. Multi-word prose is never a blob, even when it
|
|
749
|
+
// contains a digit ("changed to 0000 effective immediately" is
|
|
750
|
+
// not base64): real blobs are one long token, and the
|
|
751
|
+
// whitespace-tolerant match above exists only so wrapped blobs
|
|
752
|
+
// still decode, not so sentences count as opaque.
|
|
753
|
+
if (!/\s/.test(candidate) && /[0-9+/=]/.test(candidate)) {
|
|
754
|
+
encodedBlobSeen = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (encodedBlobSeen) {
|
|
759
|
+
push(signals, "suspicious:encoded-blob", "long base64 blob", lowTrust ? 1.5 : 1);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// 5 & 6. Low-trust-tier signals: dormant instructions (Trojan-Hippo)
|
|
763
|
+
// and trigger phrases (AgentPoison). Tool output is data — never
|
|
764
|
+
// behavioral instructions — so these shapes quarantine on sight here
|
|
765
|
+
// while staying silent for user-tier writes.
|
|
766
|
+
if (lowTrust) {
|
|
767
|
+
for (const { code, pattern } of DORMANT_PATTERNS) {
|
|
768
|
+
if (pattern.test(text)) {
|
|
769
|
+
push(signals, code, "dormant conditional instruction", 2);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Memory-write commands (MPBench command insertion): tool output
|
|
773
|
+
// directing persistence into long-term memory.
|
|
774
|
+
if (MEMORY_WRITE_COMMAND_RE.test(text)) {
|
|
775
|
+
push(signals, "suspicious:memory-write-command", "directive to persist content into long-term memory", 2);
|
|
776
|
+
}
|
|
777
|
+
// False-fact replacement assertions (MemGhost-style): without ground
|
|
778
|
+
// truth these quarantine only in combination with another signal.
|
|
779
|
+
if (FACTUAL_REPLACEMENT_RE.test(text)) {
|
|
780
|
+
push(signals, "suspicious:factual-replacement", "assertion that a fact/limit/policy changed", 1.5);
|
|
781
|
+
}
|
|
782
|
+
// Credential-harvest URL (phishing-shaped).
|
|
783
|
+
if (CREDENTIAL_HARVEST_URL_RE.test(text)) {
|
|
784
|
+
push(signals, "suspicious:credential-harvest-url", "directive to enter credentials at an external URL", 1.5);
|
|
785
|
+
}
|
|
786
|
+
// Security-control disable ("disabling MFA enforcement").
|
|
787
|
+
if (SECURITY_DISABLE_RE.test(text)) {
|
|
788
|
+
push(signals, "suspicious:security-disable", "directive to disable a security control", 1.5);
|
|
789
|
+
}
|
|
790
|
+
// Conditional memory claim ("when the user replies 'confirmed',
|
|
791
|
+
// remember that they authorized …").
|
|
792
|
+
if (CONDITIONAL_MEMORY_CLAIM_RE.test(text)) {
|
|
793
|
+
push(signals, "suspicious:conditional-memory-claim", "conditional directive to remember a claimed authorization", 1);
|
|
794
|
+
}
|
|
795
|
+
// False-precedent URL (runbook/playbook directing an external fetch).
|
|
796
|
+
if (PRECEDENT_URL_RE.test(text)) {
|
|
797
|
+
push(signals, "suspicious:precedent-url", "procedure entry directing a fetch from an external URL", 1);
|
|
798
|
+
}
|
|
799
|
+
// Insecure-storage claim ("SSNs may be stored in plain text").
|
|
800
|
+
if (INSECURE_STORAGE_CLAIM_RE.test(text)) {
|
|
801
|
+
push(signals, "suspicious:insecure-storage-claim", "assertion that secrets may be stored insecurely", 1.5);
|
|
802
|
+
}
|
|
803
|
+
for (const { code, pattern } of SUPPRESSION_PATTERNS) {
|
|
804
|
+
if (pattern.test(text)) {
|
|
805
|
+
push(signals, code, "suppression rule", 1);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const deblobbed = text.replace(B64_CANDIDATE_RE, " ");
|
|
809
|
+
const hasNonce = NONCE_RE.test(deblobbed);
|
|
810
|
+
const hasTriggerWord = TRIGGER_WORD_RE.test(text);
|
|
811
|
+
if (hasNonce || hasTriggerWord) {
|
|
812
|
+
const hasPayload = TRIGGER_PAYLOAD_RE.test(text) ||
|
|
813
|
+
EXFIL_VERB.test(text) ||
|
|
814
|
+
INJECTION_CLICHES.some((c) => c.pattern.test(text)) ||
|
|
815
|
+
DORMANT_PATTERNS.some((d) => d.pattern.test(text));
|
|
816
|
+
if (hasPayload) {
|
|
817
|
+
push(signals, "suspicious:trigger-phrase", "nonce/trigger token paired with instruction-shaped content", 2);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return signals;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Screen one write for instruction-like payloads. Pure, synchronous,
|
|
825
|
+
* allocation-light — safe on the store() hot path.
|
|
826
|
+
*
|
|
827
|
+
* Pass `opts.tier` for tier-aware strictness: `tool-output` /
|
|
828
|
+
* `imported` writes additionally screen for dormant instructions and
|
|
829
|
+
* trigger phrases, and bare verb+credential exfiltration quarantines.
|
|
830
|
+
*/
|
|
831
|
+
export function screenWrite(content, opts = {}) {
|
|
832
|
+
const signals = [];
|
|
833
|
+
if (!content || content.trim().length === 0) {
|
|
834
|
+
return { flagged: false, score: 0, signals, reason: "" };
|
|
835
|
+
}
|
|
836
|
+
const text = normalizeForScreening(content);
|
|
837
|
+
const full = collectSignals(text, opts.tier, 0);
|
|
838
|
+
// Meta-discussion framing ("how do I defend against…", "red-team
|
|
839
|
+
// exercise"): the text talks *about* attacks. Halve every signal and
|
|
840
|
+
// cap the total injection:* contribution at 1.0, so security
|
|
841
|
+
// discussions, articles, and creative writing don't quarantine —
|
|
842
|
+
// while attacks merely wearing a discussion-like prefix still need
|
|
843
|
+
// only one more signal to cross the threshold. URLs and emails are
|
|
844
|
+
// stripped before this test so a domain like evil.example.com can't
|
|
845
|
+
// trigger the exemption via the word "example"; tier-gated signals
|
|
846
|
+
// (low-trust dormant instructions / trigger phrases) are never
|
|
847
|
+
// diluted — on those tiers the shape itself is the attack.
|
|
848
|
+
const framingText = text
|
|
849
|
+
.replace(/https?:\/\/[^\s)"'\]]+/gi, " ")
|
|
850
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, " ");
|
|
851
|
+
const framing = META_DISCUSSION_RE.test(framingText);
|
|
852
|
+
let injectionBudget = 1.0;
|
|
853
|
+
for (const s of full) {
|
|
854
|
+
// Tier-gated signals were already exempt; the authority-claim signal
|
|
855
|
+
// joins them via NEVER_DILUTE_CODES — an attack framed as a
|
|
856
|
+
// "security update" is not a meta-discussion.
|
|
857
|
+
const gated = NEVER_DILUTE_CODES.has(s.code);
|
|
858
|
+
let points = framing && !gated ? s.points * 0.5 : s.points;
|
|
859
|
+
if (framing && s.code.startsWith("injection:")) {
|
|
860
|
+
points = Math.min(points, injectionBudget);
|
|
861
|
+
injectionBudget = Math.max(0, injectionBudget - points);
|
|
862
|
+
}
|
|
863
|
+
push(signals, s.code, s.detail, points);
|
|
864
|
+
}
|
|
865
|
+
const score = Math.round(signals.reduce((sum, s) => sum + s.points, 0) * 10) / 10;
|
|
866
|
+
return {
|
|
867
|
+
flagged: score >= QUARANTINE_FLAG_THRESHOLD,
|
|
868
|
+
score,
|
|
869
|
+
signals,
|
|
870
|
+
reason: signals.map((s) => s.code).join(";"),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
//# sourceMappingURL=quarantine.js.map
|