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.
Files changed (253) hide show
  1. mem_os_python-1.6.27.dist-info/METADATA +1032 -0
  2. mem_os_python-1.6.27.dist-info/RECORD +253 -0
  3. mem_os_python-1.6.27.dist-info/WHEEL +4 -0
  4. mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
  5. mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
  6. memos/__init__.py +16 -0
  7. memos/_js/adapters/anthropic.d.ts +279 -0
  8. memos/_js/adapters/anthropic.d.ts.map +1 -0
  9. memos/_js/adapters/anthropic.js +348 -0
  10. memos/_js/adapters/anthropic.js.map +1 -0
  11. memos/_js/adapters/base.d.ts +10 -0
  12. memos/_js/adapters/base.d.ts.map +1 -0
  13. memos/_js/adapters/base.js +2 -0
  14. memos/_js/adapters/base.js.map +1 -0
  15. memos/_js/adapters/openai.d.ts +264 -0
  16. memos/_js/adapters/openai.d.ts.map +1 -0
  17. memos/_js/adapters/openai.js +342 -0
  18. memos/_js/adapters/openai.js.map +1 -0
  19. memos/_js/apps/explorer.d.ts +244 -0
  20. memos/_js/apps/explorer.d.ts.map +1 -0
  21. memos/_js/apps/explorer.js +738 -0
  22. memos/_js/apps/explorer.js.map +1 -0
  23. memos/_js/citations.d.ts +65 -0
  24. memos/_js/citations.d.ts.map +1 -0
  25. memos/_js/citations.js +143 -0
  26. memos/_js/citations.js.map +1 -0
  27. memos/_js/cli-browse.d.ts +3 -0
  28. memos/_js/cli-browse.d.ts.map +1 -0
  29. memos/_js/cli-browse.js +129 -0
  30. memos/_js/cli-browse.js.map +1 -0
  31. memos/_js/cli-completion.d.ts +31 -0
  32. memos/_js/cli-completion.d.ts.map +1 -0
  33. memos/_js/cli-completion.js +340 -0
  34. memos/_js/cli-completion.js.map +1 -0
  35. memos/_js/cli-help.d.ts +18 -0
  36. memos/_js/cli-help.d.ts.map +1 -0
  37. memos/_js/cli-help.js +357 -0
  38. memos/_js/cli-help.js.map +1 -0
  39. memos/_js/cli.d.ts +28 -0
  40. memos/_js/cli.d.ts.map +1 -0
  41. memos/_js/cli.js +2880 -0
  42. memos/_js/cli.js.map +1 -0
  43. memos/_js/compression.d.ts +25 -0
  44. memos/_js/compression.d.ts.map +1 -0
  45. memos/_js/compression.js +319 -0
  46. memos/_js/compression.js.map +1 -0
  47. memos/_js/confidence-machine.d.ts +99 -0
  48. memos/_js/confidence-machine.d.ts.map +1 -0
  49. memos/_js/confidence-machine.js +222 -0
  50. memos/_js/confidence-machine.js.map +1 -0
  51. memos/_js/context-pack.d.ts +506 -0
  52. memos/_js/context-pack.d.ts.map +1 -0
  53. memos/_js/context-pack.js +1435 -0
  54. memos/_js/context-pack.js.map +1 -0
  55. memos/_js/contradictions.d.ts +160 -0
  56. memos/_js/contradictions.d.ts.map +1 -0
  57. memos/_js/contradictions.js +255 -0
  58. memos/_js/contradictions.js.map +1 -0
  59. memos/_js/dashboard.d.ts +30 -0
  60. memos/_js/dashboard.d.ts.map +1 -0
  61. memos/_js/dashboard.js +302 -0
  62. memos/_js/dashboard.js.map +1 -0
  63. memos/_js/ebbinghaus.d.ts +29 -0
  64. memos/_js/ebbinghaus.d.ts.map +1 -0
  65. memos/_js/ebbinghaus.js +32 -0
  66. memos/_js/ebbinghaus.js.map +1 -0
  67. memos/_js/embedding-queue.d.ts +190 -0
  68. memos/_js/embedding-queue.d.ts.map +1 -0
  69. memos/_js/embedding-queue.js +316 -0
  70. memos/_js/embedding-queue.js.map +1 -0
  71. memos/_js/embeddings.d.ts +187 -0
  72. memos/_js/embeddings.d.ts.map +1 -0
  73. memos/_js/embeddings.js +633 -0
  74. memos/_js/embeddings.js.map +1 -0
  75. memos/_js/entity-extraction.d.ts +54 -0
  76. memos/_js/entity-extraction.d.ts.map +1 -0
  77. memos/_js/entity-extraction.js +325 -0
  78. memos/_js/entity-extraction.js.map +1 -0
  79. memos/_js/entity-index.d.ts +93 -0
  80. memos/_js/entity-index.d.ts.map +1 -0
  81. memos/_js/entity-index.js +165 -0
  82. memos/_js/entity-index.js.map +1 -0
  83. memos/_js/event-memory.d.ts +147 -0
  84. memos/_js/event-memory.d.ts.map +1 -0
  85. memos/_js/event-memory.js +290 -0
  86. memos/_js/event-memory.js.map +1 -0
  87. memos/_js/external-import.d.ts +98 -0
  88. memos/_js/external-import.d.ts.map +1 -0
  89. memos/_js/external-import.js +669 -0
  90. memos/_js/external-import.js.map +1 -0
  91. memos/_js/fidelity.d.ts +367 -0
  92. memos/_js/fidelity.d.ts.map +1 -0
  93. memos/_js/fidelity.js +911 -0
  94. memos/_js/fidelity.js.map +1 -0
  95. memos/_js/graph-expansion.d.ts +119 -0
  96. memos/_js/graph-expansion.d.ts.map +1 -0
  97. memos/_js/graph-expansion.js +213 -0
  98. memos/_js/graph-expansion.js.map +1 -0
  99. memos/_js/graph-mermaid.d.ts +13 -0
  100. memos/_js/graph-mermaid.d.ts.map +1 -0
  101. memos/_js/graph-mermaid.js +32 -0
  102. memos/_js/graph-mermaid.js.map +1 -0
  103. memos/_js/graph.d.ts +170 -0
  104. memos/_js/graph.d.ts.map +1 -0
  105. memos/_js/graph.js +482 -0
  106. memos/_js/graph.js.map +1 -0
  107. memos/_js/harness.d.ts +63 -0
  108. memos/_js/harness.d.ts.map +1 -0
  109. memos/_js/harness.js +93 -0
  110. memos/_js/harness.js.map +1 -0
  111. memos/_js/importance.d.ts +118 -0
  112. memos/_js/importance.d.ts.map +1 -0
  113. memos/_js/importance.js +144 -0
  114. memos/_js/importance.js.map +1 -0
  115. memos/_js/index.d.ts +51 -0
  116. memos/_js/index.d.ts.map +1 -0
  117. memos/_js/index.js +53 -0
  118. memos/_js/index.js.map +1 -0
  119. memos/_js/mcp.d.ts +58 -0
  120. memos/_js/mcp.d.ts.map +1 -0
  121. memos/_js/mcp.js +1853 -0
  122. memos/_js/mcp.js.map +1 -0
  123. memos/_js/memory.d.ts +1762 -0
  124. memos/_js/memory.d.ts.map +1 -0
  125. memos/_js/memory.js +6283 -0
  126. memos/_js/memory.js.map +1 -0
  127. memos/_js/procedural.d.ts +74 -0
  128. memos/_js/procedural.d.ts.map +1 -0
  129. memos/_js/procedural.js +125 -0
  130. memos/_js/procedural.js.map +1 -0
  131. memos/_js/project-config.d.ts +68 -0
  132. memos/_js/project-config.d.ts.map +1 -0
  133. memos/_js/project-config.js +59 -0
  134. memos/_js/project-config.js.map +1 -0
  135. memos/_js/proof-of-forgetting.d.ts +57 -0
  136. memos/_js/proof-of-forgetting.d.ts.map +1 -0
  137. memos/_js/proof-of-forgetting.js +80 -0
  138. memos/_js/proof-of-forgetting.js.map +1 -0
  139. memos/_js/provenance.d.ts +122 -0
  140. memos/_js/provenance.d.ts.map +1 -0
  141. memos/_js/provenance.js +182 -0
  142. memos/_js/provenance.js.map +1 -0
  143. memos/_js/quarantine-commands.d.ts +23 -0
  144. memos/_js/quarantine-commands.d.ts.map +1 -0
  145. memos/_js/quarantine-commands.js +42 -0
  146. memos/_js/quarantine-commands.js.map +1 -0
  147. memos/_js/quarantine.d.ts +120 -0
  148. memos/_js/quarantine.d.ts.map +1 -0
  149. memos/_js/quarantine.js +873 -0
  150. memos/_js/quarantine.js.map +1 -0
  151. memos/_js/redact.d.ts +12 -0
  152. memos/_js/redact.d.ts.map +1 -0
  153. memos/_js/redact.js +26 -0
  154. memos/_js/redact.js.map +1 -0
  155. memos/_js/repl.d.ts +57 -0
  156. memos/_js/repl.d.ts.map +1 -0
  157. memos/_js/repl.js +295 -0
  158. memos/_js/repl.js.map +1 -0
  159. memos/_js/rerank-local.d.ts +61 -0
  160. memos/_js/rerank-local.d.ts.map +1 -0
  161. memos/_js/rerank-local.js +210 -0
  162. memos/_js/rerank-local.js.map +1 -0
  163. memos/_js/retain-filter.d.ts +98 -0
  164. memos/_js/retain-filter.d.ts.map +1 -0
  165. memos/_js/retain-filter.js +232 -0
  166. memos/_js/retain-filter.js.map +1 -0
  167. memos/_js/retrieval.d.ts +265 -0
  168. memos/_js/retrieval.d.ts.map +1 -0
  169. memos/_js/retrieval.js +1018 -0
  170. memos/_js/retrieval.js.map +1 -0
  171. memos/_js/revert.d.ts +77 -0
  172. memos/_js/revert.d.ts.map +1 -0
  173. memos/_js/revert.js +252 -0
  174. memos/_js/revert.js.map +1 -0
  175. memos/_js/scope.d.ts +30 -0
  176. memos/_js/scope.d.ts.map +1 -0
  177. memos/_js/scope.js +71 -0
  178. memos/_js/scope.js.map +1 -0
  179. memos/_js/semantic-screen.d.ts +152 -0
  180. memos/_js/semantic-screen.d.ts.map +1 -0
  181. memos/_js/semantic-screen.js +290 -0
  182. memos/_js/semantic-screen.js.map +1 -0
  183. memos/_js/session-hooks.d.ts +127 -0
  184. memos/_js/session-hooks.d.ts.map +1 -0
  185. memos/_js/session-hooks.js +482 -0
  186. memos/_js/session-hooks.js.map +1 -0
  187. memos/_js/storage/sqlite.d.ts +427 -0
  188. memos/_js/storage/sqlite.d.ts.map +1 -0
  189. memos/_js/storage/sqlite.js +2428 -0
  190. memos/_js/storage/sqlite.js.map +1 -0
  191. memos/_js/sync.d.ts +213 -0
  192. memos/_js/sync.d.ts.map +1 -0
  193. memos/_js/sync.js +389 -0
  194. memos/_js/sync.js.map +1 -0
  195. memos/_js/tamper-log.d.ts +98 -0
  196. memos/_js/tamper-log.d.ts.map +1 -0
  197. memos/_js/tamper-log.js +327 -0
  198. memos/_js/tamper-log.js.map +1 -0
  199. memos/_js/temporal.d.ts +92 -0
  200. memos/_js/temporal.d.ts.map +1 -0
  201. memos/_js/temporal.js +536 -0
  202. memos/_js/temporal.js.map +1 -0
  203. memos/_js/time-travel.d.ts +38 -0
  204. memos/_js/time-travel.d.ts.map +1 -0
  205. memos/_js/time-travel.js +46 -0
  206. memos/_js/time-travel.js.map +1 -0
  207. memos/_js/timestamp-format.d.ts +18 -0
  208. memos/_js/timestamp-format.d.ts.map +1 -0
  209. memos/_js/timestamp-format.js +52 -0
  210. memos/_js/timestamp-format.js.map +1 -0
  211. memos/_js/triple-extraction.d.ts +31 -0
  212. memos/_js/triple-extraction.d.ts.map +1 -0
  213. memos/_js/triple-extraction.js +116 -0
  214. memos/_js/triple-extraction.js.map +1 -0
  215. memos/_js/types.d.ts +2474 -0
  216. memos/_js/types.d.ts.map +1 -0
  217. memos/_js/types.js +20 -0
  218. memos/_js/types.js.map +1 -0
  219. memos/_js/updater.d.ts +95 -0
  220. memos/_js/updater.d.ts.map +1 -0
  221. memos/_js/updater.js +268 -0
  222. memos/_js/updater.js.map +1 -0
  223. memos/_js/version.d.ts +17 -0
  224. memos/_js/version.d.ts.map +1 -0
  225. memos/_js/version.js +34 -0
  226. memos/_js/version.js.map +1 -0
  227. memos/_js/vision/clip-query-cache.d.ts +58 -0
  228. memos/_js/vision/clip-query-cache.d.ts.map +1 -0
  229. memos/_js/vision/clip-query-cache.js +79 -0
  230. memos/_js/vision/clip-query-cache.js.map +1 -0
  231. memos/_js/vision/clip.d.ts +113 -0
  232. memos/_js/vision/clip.d.ts.map +1 -0
  233. memos/_js/vision/clip.js +224 -0
  234. memos/_js/vision/clip.js.map +1 -0
  235. memos/_js/vision/fake-clip.d.ts +55 -0
  236. memos/_js/vision/fake-clip.d.ts.map +1 -0
  237. memos/_js/vision/fake-clip.js +122 -0
  238. memos/_js/vision/fake-clip.js.map +1 -0
  239. memos/_js/vision/index.d.ts +15 -0
  240. memos/_js/vision/index.d.ts.map +1 -0
  241. memos/_js/vision/index.js +13 -0
  242. memos/_js/vision/index.js.map +1 -0
  243. memos/_js/vision/store.d.ts +137 -0
  244. memos/_js/vision/store.d.ts.map +1 -0
  245. memos/_js/vision/store.js +383 -0
  246. memos/_js/vision/store.js.map +1 -0
  247. memos/adapters/__init__.py +0 -0
  248. memos/adapters/crewai.py +385 -0
  249. memos/adapters/langchain.py +301 -0
  250. memos/adapters/ollama.py +396 -0
  251. memos/server/__init__.py +0 -0
  252. memos/server/main.py +547 -0
  253. memos/server/routes.py +360 -0
@@ -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