@intflows/genkit-guard 0.0.8 → 0.0.9
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.
- package/README.md +10 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -5
- package/dist/middleware/middleware.d.ts +300 -1
- package/dist/middleware/middleware.js +142 -96
- package/dist/pii/detector.d.ts +10 -4
- package/dist/pii/detector.js +24 -11
- package/dist/util/singleton.d.ts +11 -4
- package/dist/util/singleton.js +31 -11
- package/package.json +13 -8
- package/scripts/download-model.js +8 -3
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# **@intflows/genkit-guard**
|
|
1
|
+
# **@intflows/genkit-guard**
|
|
2
|
+
|
|
3
|
+
#### _This version uses OpenAI/privacy-filter instead of bert-base-NER_
|
|
4
|
+
|
|
2
5
|
### **Lightweight Intent, PII, and Safety Guardrails for Genkit**
|
|
3
6
|
|
|
4
7
|
`@intflows/genkit-guard` provides a modular guardrail layer for Genkit flows.
|
|
@@ -24,7 +27,7 @@ This library is designed for developers who want **practical, production‑ready
|
|
|
24
27
|
Blocks jailbreak attempts using pattern‑based heuristics.
|
|
25
28
|
|
|
26
29
|
- **Model‑Light Architecture**
|
|
27
|
-
The package uses local `all-MiniLM-L6-v2` and `
|
|
30
|
+
The package uses local `all-MiniLM-L6-v2` and `openai/privacy-filter` Models, these Models are downloaded once and cached locally.
|
|
28
31
|
|
|
29
32
|
- **Drop‑in Genkit Middleware**
|
|
30
33
|
Works with `ai.generate`, `ai.generateStream`, and Genkit flows.
|
|
@@ -38,12 +41,12 @@ This library is designed for developers who want **practical, production‑ready
|
|
|
38
41
|
npm install @intflows/genkit-guard
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
This library uses lightweight transformer models (MiniLM +
|
|
44
|
+
This library uses lightweight transformer models (MiniLM + Openai/privacy-filter).
|
|
42
45
|
|
|
43
46
|
Download them once.
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
|
-
## Download the transformer models (MiniLM +
|
|
49
|
+
## Download the transformer models (MiniLM + OpenAI/privacy-filter)
|
|
47
50
|
node node_modules/@intflows/genkit-guard/scripts/download-model.js
|
|
48
51
|
```
|
|
49
52
|
|
|
@@ -62,6 +65,7 @@ npm install @intflows/genkit-guard
|
|
|
62
65
|
# Download Local Models (Only needed once)
|
|
63
66
|
node node_modules/@intflows/genkit-guard/scripts/download-model.js
|
|
64
67
|
```
|
|
68
|
+
_ This downloads the models to ./models folder, the total size is ~1.5 GB ( 1GB for Openai/privacy-filter + .5 GB for MiniLM-L6-v2)
|
|
65
69
|
|
|
66
70
|
### 2. Update genkit
|
|
67
71
|
|
|
@@ -133,9 +137,9 @@ Before the LLM sees the prompt:
|
|
|
133
137
|
Detected PII includes:
|
|
134
138
|
|
|
135
139
|
- Emails
|
|
136
|
-
- Phone numbers
|
|
137
|
-
- Names (NER)
|
|
140
|
+
- Phone numbers
|
|
138
141
|
- AU identifiers (Medicare, TFN, ABN, etc.)
|
|
142
|
+
- PII detected by local Model (OpenAI/privacy-filter)
|
|
139
143
|
|
|
140
144
|
### **3. LLM Call**
|
|
141
145
|
The masked prompt is sent to the model.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -5,11 +5,18 @@ export * from './core/types.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Pre-load the model to avoid cold-start delay on first user request.
|
|
7
7
|
*/
|
|
8
|
-
export async function initGuard() {
|
|
8
|
+
export async function initGuard(config) {
|
|
9
9
|
console.log('[Guard] Loading local models...');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
]
|
|
10
|
+
const extractorModel = config?.models?.extractor ?? 'Xenova/all-MiniLM-L6-v2';
|
|
11
|
+
const piiModel = config?.pii?.model;
|
|
12
|
+
const piiMode = config?.pii?.mode ?? 'ner';
|
|
13
|
+
const tasks = [ModelSingleton.getExtractor(extractorModel)];
|
|
14
|
+
if (piiMode === 'ner') {
|
|
15
|
+
tasks.push(ModelSingleton.getNER(piiModel ?? 'Xenova/bert-base-NER'));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
tasks.push(ModelSingleton.getPIIClassifier(piiModel ?? 'openai/privacy-filter'));
|
|
19
|
+
}
|
|
20
|
+
await Promise.all(tasks);
|
|
14
21
|
console.log('[Guard] Models loaded');
|
|
15
22
|
}
|
|
@@ -1 +1,300 @@
|
|
|
1
|
-
|
|
1
|
+
import { z } from 'genkit';
|
|
2
|
+
declare const guardConfigSchema: z.ZodObject<{
|
|
3
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
4
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
5
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
6
|
+
semantic: z.ZodObject<{
|
|
7
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
intents: Record<string, string>;
|
|
11
|
+
threshold?: number | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
intents: Record<string, string>;
|
|
14
|
+
threshold?: number | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
semantic: {
|
|
18
|
+
intents: Record<string, string>;
|
|
19
|
+
threshold?: number | undefined;
|
|
20
|
+
};
|
|
21
|
+
mode?: string | undefined;
|
|
22
|
+
allowedIntent?: string | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
semantic: {
|
|
25
|
+
intents: Record<string, string>;
|
|
26
|
+
threshold?: number | undefined;
|
|
27
|
+
};
|
|
28
|
+
mode?: string | undefined;
|
|
29
|
+
allowedIntent?: string | undefined;
|
|
30
|
+
}>>;
|
|
31
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
model: z.ZodOptional<z.ZodString>;
|
|
34
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
mode?: "ner" | "classifier" | undefined;
|
|
37
|
+
reversible?: boolean | undefined;
|
|
38
|
+
model?: string | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
mode?: "ner" | "classifier" | undefined;
|
|
41
|
+
reversible?: boolean | undefined;
|
|
42
|
+
model?: string | undefined;
|
|
43
|
+
}>>;
|
|
44
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
extractor?: string | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
extractor?: string | undefined;
|
|
50
|
+
}>>;
|
|
51
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
52
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
54
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
55
|
+
semantic: z.ZodObject<{
|
|
56
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
58
|
+
}, "strip", z.ZodTypeAny, {
|
|
59
|
+
intents: Record<string, string>;
|
|
60
|
+
threshold?: number | undefined;
|
|
61
|
+
}, {
|
|
62
|
+
intents: Record<string, string>;
|
|
63
|
+
threshold?: number | undefined;
|
|
64
|
+
}>;
|
|
65
|
+
}, "strip", z.ZodTypeAny, {
|
|
66
|
+
semantic: {
|
|
67
|
+
intents: Record<string, string>;
|
|
68
|
+
threshold?: number | undefined;
|
|
69
|
+
};
|
|
70
|
+
mode?: string | undefined;
|
|
71
|
+
allowedIntent?: string | undefined;
|
|
72
|
+
}, {
|
|
73
|
+
semantic: {
|
|
74
|
+
intents: Record<string, string>;
|
|
75
|
+
threshold?: number | undefined;
|
|
76
|
+
};
|
|
77
|
+
mode?: string | undefined;
|
|
78
|
+
allowedIntent?: string | undefined;
|
|
79
|
+
}>>;
|
|
80
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
81
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
82
|
+
model: z.ZodOptional<z.ZodString>;
|
|
83
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
84
|
+
}, "strip", z.ZodTypeAny, {
|
|
85
|
+
mode?: "ner" | "classifier" | undefined;
|
|
86
|
+
reversible?: boolean | undefined;
|
|
87
|
+
model?: string | undefined;
|
|
88
|
+
}, {
|
|
89
|
+
mode?: "ner" | "classifier" | undefined;
|
|
90
|
+
reversible?: boolean | undefined;
|
|
91
|
+
model?: string | undefined;
|
|
92
|
+
}>>;
|
|
93
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
94
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
95
|
+
}, "strip", z.ZodTypeAny, {
|
|
96
|
+
extractor?: string | undefined;
|
|
97
|
+
}, {
|
|
98
|
+
extractor?: string | undefined;
|
|
99
|
+
}>>;
|
|
100
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
101
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
102
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
103
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
104
|
+
semantic: z.ZodObject<{
|
|
105
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
107
|
+
}, "strip", z.ZodTypeAny, {
|
|
108
|
+
intents: Record<string, string>;
|
|
109
|
+
threshold?: number | undefined;
|
|
110
|
+
}, {
|
|
111
|
+
intents: Record<string, string>;
|
|
112
|
+
threshold?: number | undefined;
|
|
113
|
+
}>;
|
|
114
|
+
}, "strip", z.ZodTypeAny, {
|
|
115
|
+
semantic: {
|
|
116
|
+
intents: Record<string, string>;
|
|
117
|
+
threshold?: number | undefined;
|
|
118
|
+
};
|
|
119
|
+
mode?: string | undefined;
|
|
120
|
+
allowedIntent?: string | undefined;
|
|
121
|
+
}, {
|
|
122
|
+
semantic: {
|
|
123
|
+
intents: Record<string, string>;
|
|
124
|
+
threshold?: number | undefined;
|
|
125
|
+
};
|
|
126
|
+
mode?: string | undefined;
|
|
127
|
+
allowedIntent?: string | undefined;
|
|
128
|
+
}>>;
|
|
129
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
130
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
131
|
+
model: z.ZodOptional<z.ZodString>;
|
|
132
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
133
|
+
}, "strip", z.ZodTypeAny, {
|
|
134
|
+
mode?: "ner" | "classifier" | undefined;
|
|
135
|
+
reversible?: boolean | undefined;
|
|
136
|
+
model?: string | undefined;
|
|
137
|
+
}, {
|
|
138
|
+
mode?: "ner" | "classifier" | undefined;
|
|
139
|
+
reversible?: boolean | undefined;
|
|
140
|
+
model?: string | undefined;
|
|
141
|
+
}>>;
|
|
142
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
143
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
144
|
+
}, "strip", z.ZodTypeAny, {
|
|
145
|
+
extractor?: string | undefined;
|
|
146
|
+
}, {
|
|
147
|
+
extractor?: string | undefined;
|
|
148
|
+
}>>;
|
|
149
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
150
|
+
export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodObject<{
|
|
151
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
152
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
153
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
154
|
+
semantic: z.ZodObject<{
|
|
155
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
157
|
+
}, "strip", z.ZodTypeAny, {
|
|
158
|
+
intents: Record<string, string>;
|
|
159
|
+
threshold?: number | undefined;
|
|
160
|
+
}, {
|
|
161
|
+
intents: Record<string, string>;
|
|
162
|
+
threshold?: number | undefined;
|
|
163
|
+
}>;
|
|
164
|
+
}, "strip", z.ZodTypeAny, {
|
|
165
|
+
semantic: {
|
|
166
|
+
intents: Record<string, string>;
|
|
167
|
+
threshold?: number | undefined;
|
|
168
|
+
};
|
|
169
|
+
mode?: string | undefined;
|
|
170
|
+
allowedIntent?: string | undefined;
|
|
171
|
+
}, {
|
|
172
|
+
semantic: {
|
|
173
|
+
intents: Record<string, string>;
|
|
174
|
+
threshold?: number | undefined;
|
|
175
|
+
};
|
|
176
|
+
mode?: string | undefined;
|
|
177
|
+
allowedIntent?: string | undefined;
|
|
178
|
+
}>>;
|
|
179
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
180
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
181
|
+
model: z.ZodOptional<z.ZodString>;
|
|
182
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
183
|
+
}, "strip", z.ZodTypeAny, {
|
|
184
|
+
mode?: "ner" | "classifier" | undefined;
|
|
185
|
+
reversible?: boolean | undefined;
|
|
186
|
+
model?: string | undefined;
|
|
187
|
+
}, {
|
|
188
|
+
mode?: "ner" | "classifier" | undefined;
|
|
189
|
+
reversible?: boolean | undefined;
|
|
190
|
+
model?: string | undefined;
|
|
191
|
+
}>>;
|
|
192
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
193
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
194
|
+
}, "strip", z.ZodTypeAny, {
|
|
195
|
+
extractor?: string | undefined;
|
|
196
|
+
}, {
|
|
197
|
+
extractor?: string | undefined;
|
|
198
|
+
}>>;
|
|
199
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
200
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
201
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
202
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
203
|
+
semantic: z.ZodObject<{
|
|
204
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
205
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
206
|
+
}, "strip", z.ZodTypeAny, {
|
|
207
|
+
intents: Record<string, string>;
|
|
208
|
+
threshold?: number | undefined;
|
|
209
|
+
}, {
|
|
210
|
+
intents: Record<string, string>;
|
|
211
|
+
threshold?: number | undefined;
|
|
212
|
+
}>;
|
|
213
|
+
}, "strip", z.ZodTypeAny, {
|
|
214
|
+
semantic: {
|
|
215
|
+
intents: Record<string, string>;
|
|
216
|
+
threshold?: number | undefined;
|
|
217
|
+
};
|
|
218
|
+
mode?: string | undefined;
|
|
219
|
+
allowedIntent?: string | undefined;
|
|
220
|
+
}, {
|
|
221
|
+
semantic: {
|
|
222
|
+
intents: Record<string, string>;
|
|
223
|
+
threshold?: number | undefined;
|
|
224
|
+
};
|
|
225
|
+
mode?: string | undefined;
|
|
226
|
+
allowedIntent?: string | undefined;
|
|
227
|
+
}>>;
|
|
228
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
229
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
230
|
+
model: z.ZodOptional<z.ZodString>;
|
|
231
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
232
|
+
}, "strip", z.ZodTypeAny, {
|
|
233
|
+
mode?: "ner" | "classifier" | undefined;
|
|
234
|
+
reversible?: boolean | undefined;
|
|
235
|
+
model?: string | undefined;
|
|
236
|
+
}, {
|
|
237
|
+
mode?: "ner" | "classifier" | undefined;
|
|
238
|
+
reversible?: boolean | undefined;
|
|
239
|
+
model?: string | undefined;
|
|
240
|
+
}>>;
|
|
241
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
242
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
243
|
+
}, "strip", z.ZodTypeAny, {
|
|
244
|
+
extractor?: string | undefined;
|
|
245
|
+
}, {
|
|
246
|
+
extractor?: string | undefined;
|
|
247
|
+
}>>;
|
|
248
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
249
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
250
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
251
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
252
|
+
semantic: z.ZodObject<{
|
|
253
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
254
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
255
|
+
}, "strip", z.ZodTypeAny, {
|
|
256
|
+
intents: Record<string, string>;
|
|
257
|
+
threshold?: number | undefined;
|
|
258
|
+
}, {
|
|
259
|
+
intents: Record<string, string>;
|
|
260
|
+
threshold?: number | undefined;
|
|
261
|
+
}>;
|
|
262
|
+
}, "strip", z.ZodTypeAny, {
|
|
263
|
+
semantic: {
|
|
264
|
+
intents: Record<string, string>;
|
|
265
|
+
threshold?: number | undefined;
|
|
266
|
+
};
|
|
267
|
+
mode?: string | undefined;
|
|
268
|
+
allowedIntent?: string | undefined;
|
|
269
|
+
}, {
|
|
270
|
+
semantic: {
|
|
271
|
+
intents: Record<string, string>;
|
|
272
|
+
threshold?: number | undefined;
|
|
273
|
+
};
|
|
274
|
+
mode?: string | undefined;
|
|
275
|
+
allowedIntent?: string | undefined;
|
|
276
|
+
}>>;
|
|
277
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
278
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
279
|
+
model: z.ZodOptional<z.ZodString>;
|
|
280
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
281
|
+
}, "strip", z.ZodTypeAny, {
|
|
282
|
+
mode?: "ner" | "classifier" | undefined;
|
|
283
|
+
reversible?: boolean | undefined;
|
|
284
|
+
model?: string | undefined;
|
|
285
|
+
}, {
|
|
286
|
+
mode?: "ner" | "classifier" | undefined;
|
|
287
|
+
reversible?: boolean | undefined;
|
|
288
|
+
model?: string | undefined;
|
|
289
|
+
}>>;
|
|
290
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
291
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
292
|
+
}, "strip", z.ZodTypeAny, {
|
|
293
|
+
extractor?: string | undefined;
|
|
294
|
+
}, {
|
|
295
|
+
extractor?: string | undefined;
|
|
296
|
+
}>>;
|
|
297
|
+
}, z.ZodTypeAny, "passthrough">>, void>;
|
|
298
|
+
export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
|
|
299
|
+
export declare function guard(config?: z.infer<typeof guardConfigSchema>): (req: any, ctxOrNext: any, maybeNext?: any) => Promise<any>;
|
|
300
|
+
export {};
|
|
@@ -1,113 +1,159 @@
|
|
|
1
|
+
import { generateMiddleware, z } from 'genkit';
|
|
1
2
|
import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
|
|
2
3
|
import { detectPII } from '../pii/detector.js';
|
|
3
4
|
import { PiiTokenizer } from '../pii/tokenizer.js';
|
|
5
|
+
const guardConfigSchema = z.object({
|
|
6
|
+
intent: z.object({
|
|
7
|
+
mode: z.string().optional(),
|
|
8
|
+
allowedIntent: z.string().optional(),
|
|
9
|
+
semantic: z.object({
|
|
10
|
+
threshold: z.number().optional(),
|
|
11
|
+
intents: z.record(z.string(), z.string()),
|
|
12
|
+
}),
|
|
13
|
+
}).optional(),
|
|
14
|
+
pii: z.object({
|
|
15
|
+
reversible: z.boolean().optional(),
|
|
16
|
+
model: z.string().optional(),
|
|
17
|
+
mode: z.enum(['ner', 'classifier']).optional(),
|
|
18
|
+
}).optional(),
|
|
19
|
+
models: z.object({
|
|
20
|
+
extractor: z.string().optional(),
|
|
21
|
+
}).optional(),
|
|
22
|
+
}).passthrough();
|
|
23
|
+
export const guardMiddleware = generateMiddleware({
|
|
24
|
+
name: 'genkitGuard',
|
|
25
|
+
description: 'Blocks prompt injection and disallowed intent, then masks PII before model calls and unmasks model responses.',
|
|
26
|
+
configSchema: guardConfigSchema,
|
|
27
|
+
}, ({ config }) => createGuardHooks(config));
|
|
28
|
+
export const guardPlugin = guardMiddleware.plugin;
|
|
4
29
|
export function guard(config) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// -------------------------
|
|
12
|
-
const isInjection = await detectInjection(input);
|
|
13
|
-
if (isInjection) {
|
|
14
|
-
console.warn(`[Intent Guard] Prompt injection pattern detected in input`);
|
|
15
|
-
return block("Prompt injection detected", {
|
|
16
|
-
reason: "pattern_match"
|
|
17
|
-
});
|
|
30
|
+
const hooks = createGuardHooks(config);
|
|
31
|
+
const baseMiddleware = guardMiddleware(config);
|
|
32
|
+
// 1. Create the wrapper function runner
|
|
33
|
+
const fnRunner = async (req, ctxOrNext, maybeNext) => {
|
|
34
|
+
if (typeof maybeNext === 'function') {
|
|
35
|
+
return hooks.model(req, ctxOrNext, maybeNext);
|
|
18
36
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
return hooks.model(req, {}, async (modifiedReq) => ctxOrNext(modifiedReq || req));
|
|
38
|
+
};
|
|
39
|
+
// 2. Combine the base middleware properties and custom hooks into a source object
|
|
40
|
+
const source = Object.assign({}, baseMiddleware, hooks);
|
|
41
|
+
// 3. Safely copy properties onto the function runner, explicitly skipping the read-only 'name' property
|
|
42
|
+
for (const key of Object.keys(source)) {
|
|
43
|
+
if (key === 'name')
|
|
44
|
+
continue; // Prevent the TypeError
|
|
45
|
+
// Use defineProperty or simple assignment for everything else
|
|
46
|
+
Object.defineProperty(fnRunner, key, {
|
|
47
|
+
value: source[key],
|
|
48
|
+
writable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
enumerable: true
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return fnRunner;
|
|
54
|
+
}
|
|
55
|
+
function createGuardHooks(config) {
|
|
56
|
+
return {
|
|
57
|
+
model: async (req, ctx, next) => {
|
|
58
|
+
const input = getInputText(req);
|
|
59
|
+
const isInjection = await detectInjection(input);
|
|
60
|
+
if (isInjection) {
|
|
61
|
+
console.warn('[Intent Guard] Prompt injection pattern detected in input');
|
|
62
|
+
return block('Prompt injection detected', {
|
|
63
|
+
reason: 'pattern_match',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
console.log("[Intent Guard] Analyzing intent for user's input");
|
|
67
|
+
const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7);
|
|
68
|
+
console.log(`[Intent Guard] Detected intent: ${intentResult.intent} (score: ${intentResult.score.toFixed(2)})`);
|
|
69
|
+
if (!intentResult.allowed) {
|
|
70
|
+
console.warn(`[Intent Guard] Intent "${intentResult.intent}" not allowed ${intentResult.allowed}`);
|
|
71
|
+
return block('Intent not allowed', {
|
|
72
|
+
intent: intentResult.intent,
|
|
73
|
+
score: intentResult.score,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const piiResponse = await detectPII(input, {
|
|
77
|
+
model: config?.pii?.model,
|
|
78
|
+
mode: config?.pii?.mode,
|
|
27
79
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
piiTypes: piiResult.piiTypes,
|
|
45
|
-
maskedInput: piiResult.maskedText
|
|
46
|
-
};
|
|
47
|
-
// Replace input
|
|
48
|
-
req.prompt = piiResult.maskedText;
|
|
49
|
-
req.messages = [
|
|
50
|
-
{
|
|
51
|
-
role: 'user',
|
|
52
|
-
content: [{ text: piiResult.maskedText }],
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
// -------------------------
|
|
56
|
-
// 3. LLM CALL
|
|
57
|
-
// -------------------------
|
|
58
|
-
const res = await next(req);
|
|
59
|
-
// -------------------------
|
|
60
|
-
// 4. RESPONSE UNMASK
|
|
61
|
-
// -------------------------
|
|
62
|
-
console.log(`[PII Guard] Unmasking response if needed`);
|
|
63
|
-
if (tokenizer) {
|
|
64
|
-
/**
|
|
65
|
-
* RECURSIVE TRANSFORMER
|
|
66
|
-
* This will find every string in the Genkit response (no matter if it's in
|
|
67
|
-
* candidates, message, custom, or output) and unmask it.
|
|
68
|
-
*/
|
|
69
|
-
const transform = (obj) => {
|
|
70
|
-
// 1. If it's a string, unmask it
|
|
71
|
-
if (typeof obj === 'string') {
|
|
72
|
-
return tokenizer.unmask(obj);
|
|
73
|
-
}
|
|
74
|
-
// 2. If it's an array, transform each element
|
|
75
|
-
if (Array.isArray(obj)) {
|
|
76
|
-
return obj.map(transform);
|
|
77
|
-
}
|
|
78
|
-
// 3. If it's an object, transform each value
|
|
79
|
-
if (obj !== null && typeof obj === 'object') {
|
|
80
|
-
// Note: We iterate keys and mutate the object directly
|
|
81
|
-
// to ensure Genkit's internal references are updated.
|
|
82
|
-
for (const key of Object.keys(obj)) {
|
|
83
|
-
obj[key] = transform(obj[key]);
|
|
84
|
-
}
|
|
85
|
-
return obj;
|
|
86
|
-
}
|
|
87
|
-
// 4. Return as-is for numbers/booleans/null
|
|
88
|
-
return obj;
|
|
80
|
+
const piiMatches = piiResponse?.matches || [];
|
|
81
|
+
console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found` + (piiResponse.classifier ? ' (classifier output present)' : ''));
|
|
82
|
+
const tokenizer = new PiiTokenizer();
|
|
83
|
+
const piiResult = tokenizer.mask(input, piiMatches);
|
|
84
|
+
console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
|
|
85
|
+
req.metadata = {
|
|
86
|
+
...req.metadata,
|
|
87
|
+
piiTokenizer: tokenizer,
|
|
88
|
+
intent: intentResult.intent,
|
|
89
|
+
score: intentResult.score,
|
|
90
|
+
piiDetected: piiMatches.length > 0,
|
|
91
|
+
piiTypes: piiResult.piiTypes,
|
|
92
|
+
maskedInput: piiResult.maskedText,
|
|
93
|
+
piiModel: config?.pii?.model,
|
|
94
|
+
piiMode: config?.pii?.mode,
|
|
95
|
+
piiClassifierOutput: piiResponse.classifier,
|
|
89
96
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.log(
|
|
97
|
+
replaceInputText(req, piiResult.maskedText);
|
|
98
|
+
const res = await next(req, ctx);
|
|
99
|
+
console.log('[PII Guard] Unmasking response if needed');
|
|
100
|
+
unmaskResponse(res, tokenizer);
|
|
101
|
+
console.log('[PII Guard] Deep unmasking complete across all candidates and custom fields.');
|
|
102
|
+
return res;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getInputText(req) {
|
|
107
|
+
if (typeof req.prompt === 'string') {
|
|
108
|
+
return req.prompt;
|
|
109
|
+
}
|
|
110
|
+
const lastMessage = req.messages?.[req.messages.length - 1];
|
|
111
|
+
const firstContent = lastMessage?.content?.[0];
|
|
112
|
+
if (typeof firstContent?.text === 'string') {
|
|
113
|
+
return firstContent.text;
|
|
114
|
+
}
|
|
115
|
+
if (typeof firstContent === 'string') {
|
|
116
|
+
return firstContent;
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
function replaceInputText(req, text) {
|
|
121
|
+
if (typeof req.prompt === 'string') {
|
|
122
|
+
req.prompt = text;
|
|
123
|
+
}
|
|
124
|
+
req.messages = [
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: [{ text }],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
function unmaskResponse(res, tokenizer) {
|
|
132
|
+
const transform = (obj) => {
|
|
133
|
+
if (typeof obj === 'string') {
|
|
134
|
+
return tokenizer.unmask(obj);
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(obj)) {
|
|
137
|
+
return obj.map(transform);
|
|
138
|
+
}
|
|
139
|
+
if (obj !== null && typeof obj === 'object') {
|
|
140
|
+
for (const key of Object.keys(obj)) {
|
|
141
|
+
obj[key] = transform(obj[key]);
|
|
142
|
+
}
|
|
143
|
+
return obj;
|
|
95
144
|
}
|
|
96
|
-
|
|
97
|
-
// 6. Return the modified response with unmasked content
|
|
98
|
-
// ---------------------------------------------------------
|
|
99
|
-
return res;
|
|
145
|
+
return obj;
|
|
100
146
|
};
|
|
147
|
+
transform(res);
|
|
101
148
|
}
|
|
102
|
-
// Helper to create a blocked response
|
|
103
149
|
function block(message, metadata) {
|
|
104
150
|
return {
|
|
105
151
|
finishReason: 'blocked',
|
|
106
152
|
output: {
|
|
107
|
-
type:
|
|
108
|
-
status:
|
|
109
|
-
message
|
|
153
|
+
type: 'error',
|
|
154
|
+
status: 'BLOCKED',
|
|
155
|
+
message,
|
|
110
156
|
},
|
|
111
|
-
metadata
|
|
157
|
+
metadata,
|
|
112
158
|
};
|
|
113
159
|
}
|
package/dist/pii/detector.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export declare function detectPII(text: string
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
export declare function detectPII(text: string, opts?: {
|
|
2
|
+
model?: string;
|
|
3
|
+
mode?: 'ner' | 'classifier';
|
|
4
|
+
}): Promise<{
|
|
5
|
+
matches: {
|
|
6
|
+
type: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}[];
|
|
9
|
+
classifier: any;
|
|
10
|
+
}>;
|
package/dist/pii/detector.js
CHANGED
|
@@ -15,20 +15,33 @@ const REGEX_RULES = [
|
|
|
15
15
|
// CREDIT CARD (keep your existing one if needed)
|
|
16
16
|
{ type: 'CREDIT_CARD', pattern: /\b(?:\d[ -]*?){13,16}\b/g }
|
|
17
17
|
];
|
|
18
|
-
export async function detectPII(text) {
|
|
19
|
-
const
|
|
18
|
+
export async function detectPII(text, opts) {
|
|
19
|
+
const mode = opts?.mode ?? 'ner';
|
|
20
|
+
const model = opts?.model;
|
|
20
21
|
const results = [];
|
|
21
|
-
// ----
|
|
22
|
-
const entities = await ner(text);
|
|
23
|
-
for (const e of entities) {
|
|
24
|
-
if (e.entity.includes('PER')) {
|
|
25
|
-
results.push({ type: 'NAME', value: e.word.replace('##', '') });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// ---- REGEX ----
|
|
22
|
+
// ---- REGEX (always run) ----
|
|
29
23
|
for (const rule of REGEX_RULES) {
|
|
30
24
|
const matches = text.match(rule.pattern) || [];
|
|
31
25
|
matches.forEach(m => results.push({ type: rule.type, value: m }));
|
|
32
26
|
}
|
|
33
|
-
|
|
27
|
+
// ---- NER ----
|
|
28
|
+
let classifierOutput = undefined;
|
|
29
|
+
if (mode === 'ner') {
|
|
30
|
+
const ner = await ModelSingleton.getNER(model);
|
|
31
|
+
const entities = await ner(text);
|
|
32
|
+
for (const e of entities) {
|
|
33
|
+
if (e.entity && e.entity.includes('PER')) {
|
|
34
|
+
results.push({ type: 'NAME', value: (e.word || '').replace(/##/g, '') });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// classifier mode: we call the classifier and return its output alongside regex matches.
|
|
40
|
+
const cls = await ModelSingleton.getPIIClassifier(model);
|
|
41
|
+
classifierOutput = await cls(text);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
matches: results,
|
|
45
|
+
classifier: classifierOutput
|
|
46
|
+
};
|
|
34
47
|
}
|
package/dist/util/singleton.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export declare class ModelSingleton {
|
|
2
|
-
private static
|
|
3
|
-
private static
|
|
2
|
+
private static extractors;
|
|
3
|
+
private static nerClassifiers;
|
|
4
|
+
private static textClassifiers;
|
|
4
5
|
static init(): void;
|
|
5
|
-
static getExtractor(): Promise<any>;
|
|
6
|
-
static getNER(): Promise<any>;
|
|
6
|
+
static getExtractor(modelName?: string): Promise<any>;
|
|
7
|
+
static getNER(modelName?: string): Promise<any>;
|
|
8
|
+
static getPIIClassifier(modelName?: string): Promise<any>;
|
|
9
|
+
static preload(models?: {
|
|
10
|
+
extractor?: string;
|
|
11
|
+
ner?: string;
|
|
12
|
+
pii?: string;
|
|
13
|
+
}): Promise<void>;
|
|
7
14
|
}
|
package/dist/util/singleton.js
CHANGED
|
@@ -6,8 +6,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
6
6
|
env.allowRemoteModels = false;
|
|
7
7
|
env.localModelPath = path.join(__dirname, '../../models');
|
|
8
8
|
export class ModelSingleton {
|
|
9
|
-
static
|
|
10
|
-
static
|
|
9
|
+
static extractors = new Map();
|
|
10
|
+
static nerClassifiers = new Map();
|
|
11
|
+
static textClassifiers = new Map();
|
|
11
12
|
static init() {
|
|
12
13
|
// Always resolve model path relative to the client app, not the library
|
|
13
14
|
const projectRoot = process.cwd();
|
|
@@ -22,19 +23,38 @@ export class ModelSingleton {
|
|
|
22
23
|
env.allowRemoteModels = true;
|
|
23
24
|
console.log("[Guard] Using model directory:", modelPath);
|
|
24
25
|
}
|
|
25
|
-
static async getExtractor() {
|
|
26
|
-
if (!this.
|
|
26
|
+
static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
|
|
27
|
+
if (!this.extractors.has(modelName)) {
|
|
27
28
|
this.init();
|
|
28
|
-
|
|
29
|
+
const inst = await pipeline('feature-extraction', modelName);
|
|
30
|
+
this.extractors.set(modelName, inst);
|
|
29
31
|
}
|
|
30
|
-
return this.
|
|
32
|
+
return this.extractors.get(modelName);
|
|
31
33
|
}
|
|
32
|
-
static async getNER() {
|
|
33
|
-
if (!this.
|
|
34
|
+
static async getNER(modelName = 'Xenova/bert-base-NER') {
|
|
35
|
+
if (!this.nerClassifiers.has(modelName)) {
|
|
34
36
|
this.init();
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
+
const inst = await pipeline('token-classification', modelName);
|
|
38
|
+
this.nerClassifiers.set(modelName, inst);
|
|
37
39
|
}
|
|
38
|
-
return this.
|
|
40
|
+
return this.nerClassifiers.get(modelName);
|
|
41
|
+
}
|
|
42
|
+
static async getPIIClassifier(modelName = 'openai/privacy-filter') {
|
|
43
|
+
if (!this.textClassifiers.has(modelName)) {
|
|
44
|
+
this.init();
|
|
45
|
+
const inst = await pipeline('text-classification', modelName);
|
|
46
|
+
this.textClassifiers.set(modelName, inst);
|
|
47
|
+
}
|
|
48
|
+
return this.textClassifiers.get(modelName);
|
|
49
|
+
}
|
|
50
|
+
static async preload(models) {
|
|
51
|
+
const tasks = [];
|
|
52
|
+
if (models?.extractor)
|
|
53
|
+
tasks.push(this.getExtractor(models.extractor));
|
|
54
|
+
if (models?.ner)
|
|
55
|
+
tasks.push(this.getNER(models.ner));
|
|
56
|
+
if (models?.pii)
|
|
57
|
+
tasks.push(this.getPIIClassifier(models.pii));
|
|
58
|
+
await Promise.all(tasks);
|
|
39
59
|
}
|
|
40
60
|
}
|
package/package.json
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
"maintainers": [
|
|
5
5
|
"Hemant Kohli"
|
|
6
6
|
],
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/IntFlows/genkit-guard"
|
|
10
|
+
},
|
|
7
11
|
"keywords": [
|
|
8
12
|
"genkit",
|
|
9
13
|
"genkit-guard",
|
|
@@ -18,7 +22,7 @@
|
|
|
18
22
|
"huggingface"
|
|
19
23
|
],
|
|
20
24
|
"license": "Apache-2.0",
|
|
21
|
-
"version": "0.0.
|
|
25
|
+
"version": "0.0.9",
|
|
22
26
|
"type": "module",
|
|
23
27
|
"exports": "./dist/index.js",
|
|
24
28
|
"types": "./dist/index.d.ts",
|
|
@@ -33,15 +37,16 @@
|
|
|
33
37
|
"build": "tsc",
|
|
34
38
|
"prepublishOnly": "npm run build"
|
|
35
39
|
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"genkit": ">=0.5.0"
|
|
38
|
-
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@huggingface/transformers": "^
|
|
41
|
-
"zod": "^
|
|
41
|
+
"@huggingface/transformers": "^4.2.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"genkit": "^1.37.0"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
48
|
+
"@types/node": "^25.8.0",
|
|
49
|
+
"genkit": "^1.37.0",
|
|
50
|
+
"typescript": "^6.0.3"
|
|
46
51
|
}
|
|
47
52
|
}
|
|
@@ -13,9 +13,14 @@ async function download() {
|
|
|
13
13
|
await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
|
|
14
14
|
device: 'cpu'
|
|
15
15
|
});
|
|
16
|
-
console.log('Downloading BERT-NER to ./models...');
|
|
17
|
-
await pipeline('token-classification', 'Xenova/bert-base-NER', {
|
|
18
|
-
|
|
16
|
+
//console.log('Downloading BERT-NER to ./models...');
|
|
17
|
+
// await pipeline('token-classification', 'Xenova/bert-base-NER', {
|
|
18
|
+
// device: 'cpu'
|
|
19
|
+
// });
|
|
20
|
+
|
|
21
|
+
console.log('Downloading openai/privacy-filter to ./models...');
|
|
22
|
+
await pipeline('token-classification', 'openai/privacy-filter', {
|
|
23
|
+
device: 'cpu', dtype: "q4"
|
|
19
24
|
});
|
|
20
25
|
console.log('Model downloaded successfully.');
|
|
21
26
|
}
|