@intflows/genkit-guard 0.0.8 → 0.0.9-alpha.1
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 +2 -2
- package/dist/index.js +35 -8
- package/dist/middleware/middleware.d.ts +380 -1
- package/dist/middleware/middleware.js +296 -93
- package/dist/pii/detector.d.ts +10 -4
- package/dist/pii/detector.js +24 -11
- package/dist/pii/tokenizer.d.ts +1 -0
- package/dist/pii/tokenizer.js +11 -2
- package/dist/util/singleton.d.ts +11 -4
- package/dist/util/singleton.js +47 -12
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { guard } from './middleware/middleware.js';
|
|
1
|
+
export { guard, guardAction, guardMiddleware, guardPlugin } from './middleware/middleware.js';
|
|
2
2
|
export * from './core/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Pre-load the model to avoid cold-start delay on first user request.
|
|
5
5
|
*/
|
|
6
|
-
export declare function initGuard(): Promise<void>;
|
|
6
|
+
export declare function initGuard(config?: any): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
import { ModelSingleton } from './util/singleton.js';
|
|
2
2
|
// export { intentGuard, piiGuard } from './middleware/middleware.js';
|
|
3
|
-
export { guard } from './middleware/middleware.js';
|
|
3
|
+
export { guard, guardAction, guardMiddleware, guardPlugin } from './middleware/middleware.js';
|
|
4
4
|
export * from './core/types.js';
|
|
5
|
+
function logGuardEvent(eventName, body, attributes = {}) {
|
|
6
|
+
console.log(JSON.stringify({
|
|
7
|
+
timestamp: new Date().toISOString(),
|
|
8
|
+
severityText: 'INFO',
|
|
9
|
+
severityNumber: 9,
|
|
10
|
+
body,
|
|
11
|
+
resource: {
|
|
12
|
+
attributes: {
|
|
13
|
+
'service.name': '@intflows/genkit-guard',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
attributes: {
|
|
17
|
+
'event.name': eventName,
|
|
18
|
+
'code.namespace': 'genkit-guard',
|
|
19
|
+
...attributes,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
5
23
|
/**
|
|
6
24
|
* Pre-load the model to avoid cold-start delay on first user request.
|
|
7
25
|
*/
|
|
8
|
-
export async function initGuard() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
]
|
|
14
|
-
|
|
26
|
+
export async function initGuard(config) {
|
|
27
|
+
logGuardEvent('guard.models.loading', 'Loading local guard models');
|
|
28
|
+
const extractorModel = config?.models?.extractor ?? 'Xenova/all-MiniLM-L6-v2';
|
|
29
|
+
const piiModel = config?.pii?.model;
|
|
30
|
+
const piiMode = config?.pii?.mode ?? 'ner';
|
|
31
|
+
const tasks = [ModelSingleton.getExtractor(extractorModel)];
|
|
32
|
+
if (piiMode === 'ner') {
|
|
33
|
+
tasks.push(ModelSingleton.getNER(piiModel ?? 'Xenova/bert-base-NER'));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
tasks.push(ModelSingleton.getPIIClassifier(piiModel ?? 'openai/privacy-filter'));
|
|
37
|
+
}
|
|
38
|
+
await Promise.all(tasks);
|
|
39
|
+
logGuardEvent('guard.models.loaded', 'Local guard models loaded', {
|
|
40
|
+
piiMode,
|
|
41
|
+
});
|
|
15
42
|
}
|
|
@@ -1 +1,380 @@
|
|
|
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
|
+
logging: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
46
|
+
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
47
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, "strip", z.ZodTypeAny, {
|
|
49
|
+
enabled?: boolean | undefined;
|
|
50
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
51
|
+
serviceName?: string | undefined;
|
|
52
|
+
}, {
|
|
53
|
+
enabled?: boolean | undefined;
|
|
54
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
55
|
+
serviceName?: string | undefined;
|
|
56
|
+
}>>;
|
|
57
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, "strip", z.ZodTypeAny, {
|
|
60
|
+
extractor?: string | undefined;
|
|
61
|
+
}, {
|
|
62
|
+
extractor?: string | undefined;
|
|
63
|
+
}>>;
|
|
64
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
65
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
66
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
67
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
68
|
+
semantic: z.ZodObject<{
|
|
69
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
71
|
+
}, "strip", z.ZodTypeAny, {
|
|
72
|
+
intents: Record<string, string>;
|
|
73
|
+
threshold?: number | undefined;
|
|
74
|
+
}, {
|
|
75
|
+
intents: Record<string, string>;
|
|
76
|
+
threshold?: number | undefined;
|
|
77
|
+
}>;
|
|
78
|
+
}, "strip", z.ZodTypeAny, {
|
|
79
|
+
semantic: {
|
|
80
|
+
intents: Record<string, string>;
|
|
81
|
+
threshold?: number | undefined;
|
|
82
|
+
};
|
|
83
|
+
mode?: string | undefined;
|
|
84
|
+
allowedIntent?: string | undefined;
|
|
85
|
+
}, {
|
|
86
|
+
semantic: {
|
|
87
|
+
intents: Record<string, string>;
|
|
88
|
+
threshold?: number | undefined;
|
|
89
|
+
};
|
|
90
|
+
mode?: string | undefined;
|
|
91
|
+
allowedIntent?: string | undefined;
|
|
92
|
+
}>>;
|
|
93
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
94
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
95
|
+
model: z.ZodOptional<z.ZodString>;
|
|
96
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
97
|
+
}, "strip", z.ZodTypeAny, {
|
|
98
|
+
mode?: "ner" | "classifier" | undefined;
|
|
99
|
+
reversible?: boolean | undefined;
|
|
100
|
+
model?: string | undefined;
|
|
101
|
+
}, {
|
|
102
|
+
mode?: "ner" | "classifier" | undefined;
|
|
103
|
+
reversible?: boolean | undefined;
|
|
104
|
+
model?: string | undefined;
|
|
105
|
+
}>>;
|
|
106
|
+
logging: z.ZodOptional<z.ZodObject<{
|
|
107
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
108
|
+
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
109
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
110
|
+
}, "strip", z.ZodTypeAny, {
|
|
111
|
+
enabled?: boolean | undefined;
|
|
112
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
113
|
+
serviceName?: string | undefined;
|
|
114
|
+
}, {
|
|
115
|
+
enabled?: boolean | undefined;
|
|
116
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
117
|
+
serviceName?: string | undefined;
|
|
118
|
+
}>>;
|
|
119
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
120
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
121
|
+
}, "strip", z.ZodTypeAny, {
|
|
122
|
+
extractor?: string | undefined;
|
|
123
|
+
}, {
|
|
124
|
+
extractor?: string | undefined;
|
|
125
|
+
}>>;
|
|
126
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
127
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
128
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
129
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
130
|
+
semantic: z.ZodObject<{
|
|
131
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
132
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
133
|
+
}, "strip", z.ZodTypeAny, {
|
|
134
|
+
intents: Record<string, string>;
|
|
135
|
+
threshold?: number | undefined;
|
|
136
|
+
}, {
|
|
137
|
+
intents: Record<string, string>;
|
|
138
|
+
threshold?: number | undefined;
|
|
139
|
+
}>;
|
|
140
|
+
}, "strip", z.ZodTypeAny, {
|
|
141
|
+
semantic: {
|
|
142
|
+
intents: Record<string, string>;
|
|
143
|
+
threshold?: number | undefined;
|
|
144
|
+
};
|
|
145
|
+
mode?: string | undefined;
|
|
146
|
+
allowedIntent?: string | undefined;
|
|
147
|
+
}, {
|
|
148
|
+
semantic: {
|
|
149
|
+
intents: Record<string, string>;
|
|
150
|
+
threshold?: number | undefined;
|
|
151
|
+
};
|
|
152
|
+
mode?: string | undefined;
|
|
153
|
+
allowedIntent?: string | undefined;
|
|
154
|
+
}>>;
|
|
155
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
156
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
157
|
+
model: z.ZodOptional<z.ZodString>;
|
|
158
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
159
|
+
}, "strip", z.ZodTypeAny, {
|
|
160
|
+
mode?: "ner" | "classifier" | undefined;
|
|
161
|
+
reversible?: boolean | undefined;
|
|
162
|
+
model?: string | undefined;
|
|
163
|
+
}, {
|
|
164
|
+
mode?: "ner" | "classifier" | undefined;
|
|
165
|
+
reversible?: boolean | undefined;
|
|
166
|
+
model?: string | undefined;
|
|
167
|
+
}>>;
|
|
168
|
+
logging: z.ZodOptional<z.ZodObject<{
|
|
169
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
170
|
+
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
171
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
172
|
+
}, "strip", z.ZodTypeAny, {
|
|
173
|
+
enabled?: boolean | undefined;
|
|
174
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
175
|
+
serviceName?: string | undefined;
|
|
176
|
+
}, {
|
|
177
|
+
enabled?: boolean | undefined;
|
|
178
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
179
|
+
serviceName?: string | undefined;
|
|
180
|
+
}>>;
|
|
181
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
182
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
183
|
+
}, "strip", z.ZodTypeAny, {
|
|
184
|
+
extractor?: string | undefined;
|
|
185
|
+
}, {
|
|
186
|
+
extractor?: string | undefined;
|
|
187
|
+
}>>;
|
|
188
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
189
|
+
type GuardConfig = z.infer<typeof guardConfigSchema>;
|
|
190
|
+
export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodObject<{
|
|
191
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
192
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
193
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
194
|
+
semantic: z.ZodObject<{
|
|
195
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
196
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
197
|
+
}, "strip", z.ZodTypeAny, {
|
|
198
|
+
intents: Record<string, string>;
|
|
199
|
+
threshold?: number | undefined;
|
|
200
|
+
}, {
|
|
201
|
+
intents: Record<string, string>;
|
|
202
|
+
threshold?: number | undefined;
|
|
203
|
+
}>;
|
|
204
|
+
}, "strip", z.ZodTypeAny, {
|
|
205
|
+
semantic: {
|
|
206
|
+
intents: Record<string, string>;
|
|
207
|
+
threshold?: number | undefined;
|
|
208
|
+
};
|
|
209
|
+
mode?: string | undefined;
|
|
210
|
+
allowedIntent?: string | undefined;
|
|
211
|
+
}, {
|
|
212
|
+
semantic: {
|
|
213
|
+
intents: Record<string, string>;
|
|
214
|
+
threshold?: number | undefined;
|
|
215
|
+
};
|
|
216
|
+
mode?: string | undefined;
|
|
217
|
+
allowedIntent?: string | undefined;
|
|
218
|
+
}>>;
|
|
219
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
220
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
221
|
+
model: z.ZodOptional<z.ZodString>;
|
|
222
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
223
|
+
}, "strip", z.ZodTypeAny, {
|
|
224
|
+
mode?: "ner" | "classifier" | undefined;
|
|
225
|
+
reversible?: boolean | undefined;
|
|
226
|
+
model?: string | undefined;
|
|
227
|
+
}, {
|
|
228
|
+
mode?: "ner" | "classifier" | undefined;
|
|
229
|
+
reversible?: boolean | undefined;
|
|
230
|
+
model?: string | undefined;
|
|
231
|
+
}>>;
|
|
232
|
+
logging: z.ZodOptional<z.ZodObject<{
|
|
233
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
234
|
+
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
235
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
236
|
+
}, "strip", z.ZodTypeAny, {
|
|
237
|
+
enabled?: boolean | undefined;
|
|
238
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
239
|
+
serviceName?: string | undefined;
|
|
240
|
+
}, {
|
|
241
|
+
enabled?: boolean | undefined;
|
|
242
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
243
|
+
serviceName?: string | undefined;
|
|
244
|
+
}>>;
|
|
245
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
246
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
247
|
+
}, "strip", z.ZodTypeAny, {
|
|
248
|
+
extractor?: string | undefined;
|
|
249
|
+
}, {
|
|
250
|
+
extractor?: string | undefined;
|
|
251
|
+
}>>;
|
|
252
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
253
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
254
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
255
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
256
|
+
semantic: z.ZodObject<{
|
|
257
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
258
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
259
|
+
}, "strip", z.ZodTypeAny, {
|
|
260
|
+
intents: Record<string, string>;
|
|
261
|
+
threshold?: number | undefined;
|
|
262
|
+
}, {
|
|
263
|
+
intents: Record<string, string>;
|
|
264
|
+
threshold?: number | undefined;
|
|
265
|
+
}>;
|
|
266
|
+
}, "strip", z.ZodTypeAny, {
|
|
267
|
+
semantic: {
|
|
268
|
+
intents: Record<string, string>;
|
|
269
|
+
threshold?: number | undefined;
|
|
270
|
+
};
|
|
271
|
+
mode?: string | undefined;
|
|
272
|
+
allowedIntent?: string | undefined;
|
|
273
|
+
}, {
|
|
274
|
+
semantic: {
|
|
275
|
+
intents: Record<string, string>;
|
|
276
|
+
threshold?: number | undefined;
|
|
277
|
+
};
|
|
278
|
+
mode?: string | undefined;
|
|
279
|
+
allowedIntent?: string | undefined;
|
|
280
|
+
}>>;
|
|
281
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
282
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
283
|
+
model: z.ZodOptional<z.ZodString>;
|
|
284
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
285
|
+
}, "strip", z.ZodTypeAny, {
|
|
286
|
+
mode?: "ner" | "classifier" | undefined;
|
|
287
|
+
reversible?: boolean | undefined;
|
|
288
|
+
model?: string | undefined;
|
|
289
|
+
}, {
|
|
290
|
+
mode?: "ner" | "classifier" | undefined;
|
|
291
|
+
reversible?: boolean | undefined;
|
|
292
|
+
model?: string | undefined;
|
|
293
|
+
}>>;
|
|
294
|
+
logging: z.ZodOptional<z.ZodObject<{
|
|
295
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
296
|
+
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
297
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
298
|
+
}, "strip", z.ZodTypeAny, {
|
|
299
|
+
enabled?: boolean | undefined;
|
|
300
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
301
|
+
serviceName?: string | undefined;
|
|
302
|
+
}, {
|
|
303
|
+
enabled?: boolean | undefined;
|
|
304
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
305
|
+
serviceName?: string | undefined;
|
|
306
|
+
}>>;
|
|
307
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
308
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
309
|
+
}, "strip", z.ZodTypeAny, {
|
|
310
|
+
extractor?: string | undefined;
|
|
311
|
+
}, {
|
|
312
|
+
extractor?: string | undefined;
|
|
313
|
+
}>>;
|
|
314
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
315
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
316
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
317
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
318
|
+
semantic: z.ZodObject<{
|
|
319
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
320
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
321
|
+
}, "strip", z.ZodTypeAny, {
|
|
322
|
+
intents: Record<string, string>;
|
|
323
|
+
threshold?: number | undefined;
|
|
324
|
+
}, {
|
|
325
|
+
intents: Record<string, string>;
|
|
326
|
+
threshold?: number | undefined;
|
|
327
|
+
}>;
|
|
328
|
+
}, "strip", z.ZodTypeAny, {
|
|
329
|
+
semantic: {
|
|
330
|
+
intents: Record<string, string>;
|
|
331
|
+
threshold?: number | undefined;
|
|
332
|
+
};
|
|
333
|
+
mode?: string | undefined;
|
|
334
|
+
allowedIntent?: string | undefined;
|
|
335
|
+
}, {
|
|
336
|
+
semantic: {
|
|
337
|
+
intents: Record<string, string>;
|
|
338
|
+
threshold?: number | undefined;
|
|
339
|
+
};
|
|
340
|
+
mode?: string | undefined;
|
|
341
|
+
allowedIntent?: string | undefined;
|
|
342
|
+
}>>;
|
|
343
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
344
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
345
|
+
model: z.ZodOptional<z.ZodString>;
|
|
346
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
347
|
+
}, "strip", z.ZodTypeAny, {
|
|
348
|
+
mode?: "ner" | "classifier" | undefined;
|
|
349
|
+
reversible?: boolean | undefined;
|
|
350
|
+
model?: string | undefined;
|
|
351
|
+
}, {
|
|
352
|
+
mode?: "ner" | "classifier" | undefined;
|
|
353
|
+
reversible?: boolean | undefined;
|
|
354
|
+
model?: string | undefined;
|
|
355
|
+
}>>;
|
|
356
|
+
logging: z.ZodOptional<z.ZodObject<{
|
|
357
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
358
|
+
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
359
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
360
|
+
}, "strip", z.ZodTypeAny, {
|
|
361
|
+
enabled?: boolean | undefined;
|
|
362
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
363
|
+
serviceName?: string | undefined;
|
|
364
|
+
}, {
|
|
365
|
+
enabled?: boolean | undefined;
|
|
366
|
+
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
367
|
+
serviceName?: string | undefined;
|
|
368
|
+
}>>;
|
|
369
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
370
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
371
|
+
}, "strip", z.ZodTypeAny, {
|
|
372
|
+
extractor?: string | undefined;
|
|
373
|
+
}, {
|
|
374
|
+
extractor?: string | undefined;
|
|
375
|
+
}>>;
|
|
376
|
+
}, z.ZodTypeAny, "passthrough">>, void>;
|
|
377
|
+
export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
|
|
378
|
+
export declare function guard(config?: GuardConfig): (req: any, ctxOrNext: any, maybeNext?: any) => Promise<any>;
|
|
379
|
+
export declare const guardAction: typeof guard;
|
|
380
|
+
export {};
|
|
@@ -1,113 +1,316 @@
|
|
|
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 GUARD_CONTEXT_KEY = '__genkitGuard';
|
|
6
|
+
const guardConfigSchema = z.object({
|
|
7
|
+
intent: z.object({
|
|
8
|
+
mode: z.string().optional(),
|
|
9
|
+
allowedIntent: z.string().optional(),
|
|
10
|
+
semantic: z.object({
|
|
11
|
+
threshold: z.number().optional(),
|
|
12
|
+
intents: z.record(z.string(), z.string()),
|
|
13
|
+
}),
|
|
14
|
+
}).optional(),
|
|
15
|
+
pii: z.object({
|
|
16
|
+
reversible: z.boolean().optional(),
|
|
17
|
+
model: z.string().optional(),
|
|
18
|
+
mode: z.enum(['ner', 'classifier']).optional(),
|
|
19
|
+
}).optional(),
|
|
20
|
+
logging: z.object({
|
|
21
|
+
enabled: z.boolean().optional(),
|
|
22
|
+
level: z.enum(['debug', 'info', 'warn', 'error']).optional(),
|
|
23
|
+
serviceName: z.string().optional(),
|
|
24
|
+
}).optional(),
|
|
25
|
+
models: z.object({
|
|
26
|
+
extractor: z.string().optional(),
|
|
27
|
+
}).optional(),
|
|
28
|
+
}).passthrough();
|
|
29
|
+
export const guardMiddleware = generateMiddleware({
|
|
30
|
+
name: 'genkitGuard',
|
|
31
|
+
description: 'Blocks prompt injection and disallowed intent, masks PII before model calls, restores PII for tool calls, and audits tool PII access.',
|
|
32
|
+
configSchema: guardConfigSchema,
|
|
33
|
+
}, ({ config }) => createGuardHooks(config));
|
|
34
|
+
export const guardPlugin = guardMiddleware.plugin;
|
|
4
35
|
export function guard(config) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// 1. INTENT ANALYSIS
|
|
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
|
-
});
|
|
36
|
+
const hooks = createGuardHooks(config);
|
|
37
|
+
const baseMiddleware = guardMiddleware(config);
|
|
38
|
+
const fnRunner = async (req, ctxOrNext, maybeNext) => {
|
|
39
|
+
if (typeof maybeNext === 'function') {
|
|
40
|
+
return hooks.model(req, ctxOrNext, maybeNext);
|
|
18
41
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
return hooks.model(req, {}, async (modifiedReq) => ctxOrNext(modifiedReq || req));
|
|
43
|
+
};
|
|
44
|
+
const source = Object.assign({}, baseMiddleware, hooks);
|
|
45
|
+
for (const key of Object.keys(source)) {
|
|
46
|
+
if (key === 'name')
|
|
47
|
+
continue;
|
|
48
|
+
Object.defineProperty(fnRunner, key, {
|
|
49
|
+
value: source[key],
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
enumerable: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return fnRunner;
|
|
56
|
+
}
|
|
57
|
+
export const guardAction = guard;
|
|
58
|
+
function createGuardHooks(config) {
|
|
59
|
+
const logger = createLogger(config);
|
|
60
|
+
return {
|
|
61
|
+
model: async (req, ctx, next) => {
|
|
62
|
+
const input = getInputText(req);
|
|
63
|
+
logger('info', 'guard.model.start', 'Starting guard checks for model request');
|
|
64
|
+
const isInjection = await detectInjection(input);
|
|
65
|
+
if (isInjection) {
|
|
66
|
+
logger('warn', 'guard.intent.blocked', 'Prompt injection pattern detected', {
|
|
67
|
+
reason: 'pattern_match',
|
|
68
|
+
});
|
|
69
|
+
return block('Prompt injection detected', {
|
|
70
|
+
reason: 'pattern_match',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
logger('info', 'guard.intent.analysis.start', 'Analyzing request intent');
|
|
74
|
+
const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7);
|
|
75
|
+
logger('info', 'guard.intent.analysis.complete', 'Intent analysis completed', {
|
|
25
76
|
intent: intentResult.intent,
|
|
26
|
-
score: intentResult.score
|
|
77
|
+
score: roundScore(intentResult.score),
|
|
78
|
+
allowed: intentResult.allowed,
|
|
79
|
+
});
|
|
80
|
+
if (!intentResult.allowed) {
|
|
81
|
+
logger('warn', 'guard.intent.blocked', 'Intent not allowed', {
|
|
82
|
+
intent: intentResult.intent,
|
|
83
|
+
score: roundScore(intentResult.score),
|
|
84
|
+
});
|
|
85
|
+
return block('Intent not allowed', {
|
|
86
|
+
intent: intentResult.intent,
|
|
87
|
+
score: intentResult.score,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const textForPii = collectModelRequestText(req);
|
|
91
|
+
const piiResponse = await scanPII(textForPii, config);
|
|
92
|
+
const piiMatches = piiResponse?.matches || [];
|
|
93
|
+
const tokenizer = new PiiTokenizer();
|
|
94
|
+
const piiTypes = uniqueTypes(piiMatches);
|
|
95
|
+
maskModelRequest(req, tokenizer, piiMatches);
|
|
96
|
+
pushTokenizer(ctx, tokenizer);
|
|
97
|
+
logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.model.pii.masked', 'PII scan completed for model request', {
|
|
98
|
+
piiDetected: piiMatches.length > 0,
|
|
99
|
+
piiMatchCount: piiMatches.length,
|
|
100
|
+
piiTypes,
|
|
101
|
+
piiMode: config?.pii?.mode ?? 'ner',
|
|
102
|
+
classifierOutputPresent: Boolean(piiResponse.classifier),
|
|
103
|
+
});
|
|
104
|
+
req.metadata = {
|
|
105
|
+
...req.metadata,
|
|
106
|
+
intent: intentResult.intent,
|
|
107
|
+
score: intentResult.score,
|
|
108
|
+
piiDetected: piiMatches.length > 0,
|
|
109
|
+
piiTypes,
|
|
110
|
+
maskedInput: getInputText(req),
|
|
111
|
+
piiModel: config?.pii?.model,
|
|
112
|
+
piiMode: config?.pii?.mode,
|
|
113
|
+
piiClassifierOutput: piiResponse.classifier,
|
|
114
|
+
};
|
|
115
|
+
const res = await next(req, ctx);
|
|
116
|
+
const unmaskedResponse = unmaskObject(res, [tokenizer]);
|
|
117
|
+
logger('info', 'guard.model.response.unmasked', 'Model response unmasked for downstream execution', {
|
|
118
|
+
piiTypes,
|
|
119
|
+
});
|
|
120
|
+
return unmaskedResponse;
|
|
121
|
+
},
|
|
122
|
+
tool: async (req, ctx, next) => {
|
|
123
|
+
const state = getGuardState(ctx);
|
|
124
|
+
const toolName = req?.toolRequest?.name;
|
|
125
|
+
if (req?.toolRequest && 'input' in req.toolRequest) {
|
|
126
|
+
req.toolRequest.input = unmaskObject(req.toolRequest.input, state.tokenizers);
|
|
127
|
+
}
|
|
128
|
+
const toolInputText = collectStrings(req?.toolRequest?.input).join('\n');
|
|
129
|
+
const piiResponse = await scanPII(toolInputText, config);
|
|
130
|
+
const piiMatches = piiResponse?.matches || [];
|
|
131
|
+
const piiTypes = uniqueTypes(piiMatches);
|
|
132
|
+
req.metadata = {
|
|
133
|
+
...req.metadata,
|
|
134
|
+
piiDetected: piiMatches.length > 0,
|
|
135
|
+
piiTypes,
|
|
136
|
+
piiMatchCount: piiMatches.length,
|
|
137
|
+
};
|
|
138
|
+
logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.tool.pii.checked', 'Tool request PII scan completed', {
|
|
139
|
+
toolName,
|
|
140
|
+
piiDetected: piiMatches.length > 0,
|
|
141
|
+
piiMatchCount: piiMatches.length,
|
|
142
|
+
piiTypes,
|
|
27
143
|
});
|
|
144
|
+
const res = await next(req, ctx);
|
|
145
|
+
const toolResponseText = collectStrings(res).join('\n');
|
|
146
|
+
if (toolResponseText) {
|
|
147
|
+
const responsePii = await scanPII(toolResponseText, config);
|
|
148
|
+
const responseMatches = responsePii?.matches || [];
|
|
149
|
+
logger(responseMatches.length > 0 ? 'warn' : 'info', 'guard.tool.response.pii.checked', 'Tool response PII scan completed', {
|
|
150
|
+
toolName,
|
|
151
|
+
piiDetected: responseMatches.length > 0,
|
|
152
|
+
piiMatchCount: responseMatches.length,
|
|
153
|
+
piiTypes: uniqueTypes(responseMatches),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return res;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function getInputText(req) {
|
|
161
|
+
if (typeof req.prompt === 'string') {
|
|
162
|
+
return req.prompt;
|
|
163
|
+
}
|
|
164
|
+
const lastMessage = req.messages?.[req.messages.length - 1];
|
|
165
|
+
const firstContent = lastMessage?.content?.[0];
|
|
166
|
+
if (typeof firstContent?.text === 'string') {
|
|
167
|
+
return firstContent.text;
|
|
168
|
+
}
|
|
169
|
+
if (typeof firstContent === 'string') {
|
|
170
|
+
return firstContent;
|
|
171
|
+
}
|
|
172
|
+
return collectStrings(lastMessage).join('\n');
|
|
173
|
+
}
|
|
174
|
+
function collectModelRequestText(req) {
|
|
175
|
+
return [
|
|
176
|
+
...collectStrings(req?.prompt),
|
|
177
|
+
...collectStrings(req?.messages),
|
|
178
|
+
...collectStrings(req?.docs),
|
|
179
|
+
].join('\n');
|
|
180
|
+
}
|
|
181
|
+
function maskModelRequest(req, tokenizer, matches) {
|
|
182
|
+
if (typeof req.prompt === 'string') {
|
|
183
|
+
req.prompt = tokenizer.mask(req.prompt, matches).maskedText;
|
|
184
|
+
}
|
|
185
|
+
if (req.messages) {
|
|
186
|
+
req.messages = transformStrings(req.messages, (value) => tokenizer.mask(value, matches).maskedText);
|
|
187
|
+
}
|
|
188
|
+
if (req.docs) {
|
|
189
|
+
req.docs = transformStrings(req.docs, (value) => tokenizer.mask(value, matches).maskedText);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function unmaskObject(obj, tokenizers) {
|
|
193
|
+
return transformStrings(obj, (value) => {
|
|
194
|
+
let result = value;
|
|
195
|
+
for (const tokenizer of tokenizers) {
|
|
196
|
+
result = tokenizer.unmask(result);
|
|
28
197
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
198
|
+
return result;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function transformStrings(obj, transform) {
|
|
202
|
+
if (typeof obj === 'string') {
|
|
203
|
+
return transform(obj);
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(obj)) {
|
|
206
|
+
for (let i = 0; i < obj.length; i++) {
|
|
207
|
+
obj[i] = transformStrings(obj[i], transform);
|
|
208
|
+
}
|
|
209
|
+
return obj;
|
|
210
|
+
}
|
|
211
|
+
if (obj !== null && typeof obj === 'object') {
|
|
212
|
+
for (const key of Object.keys(obj)) {
|
|
213
|
+
obj[key] = transformStrings(obj[key], transform);
|
|
214
|
+
}
|
|
215
|
+
return obj;
|
|
216
|
+
}
|
|
217
|
+
return obj;
|
|
218
|
+
}
|
|
219
|
+
function collectStrings(obj) {
|
|
220
|
+
if (typeof obj === 'string') {
|
|
221
|
+
return [obj];
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(obj)) {
|
|
224
|
+
return obj.flatMap(collectStrings);
|
|
225
|
+
}
|
|
226
|
+
if (obj !== null && typeof obj === 'object') {
|
|
227
|
+
return Object.values(obj).flatMap(collectStrings);
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
async function scanPII(text, config) {
|
|
232
|
+
if (!text.trim()) {
|
|
233
|
+
return {
|
|
234
|
+
matches: [],
|
|
235
|
+
classifier: undefined,
|
|
46
236
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
237
|
+
}
|
|
238
|
+
return detectPII(text, {
|
|
239
|
+
model: config?.pii?.model,
|
|
240
|
+
mode: config?.pii?.mode,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function getGuardState(ctx = {}) {
|
|
244
|
+
ctx.context = ctx.context || {};
|
|
245
|
+
ctx.context[GUARD_CONTEXT_KEY] = ctx.context[GUARD_CONTEXT_KEY] || { tokenizers: [] };
|
|
246
|
+
return ctx.context[GUARD_CONTEXT_KEY];
|
|
247
|
+
}
|
|
248
|
+
function pushTokenizer(ctx, tokenizer) {
|
|
249
|
+
const state = getGuardState(ctx);
|
|
250
|
+
state.tokenizers.push(tokenizer);
|
|
251
|
+
}
|
|
252
|
+
function uniqueTypes(matches) {
|
|
253
|
+
return Array.from(new Set(matches.map((match) => match.type.toLowerCase())));
|
|
254
|
+
}
|
|
255
|
+
function roundScore(score) {
|
|
256
|
+
return Math.round(score * 10000) / 10000;
|
|
257
|
+
}
|
|
258
|
+
function createLogger(config) {
|
|
259
|
+
const enabled = config?.logging?.enabled ?? true;
|
|
260
|
+
const minimumLevel = config?.logging?.level ?? 'info';
|
|
261
|
+
const serviceName = config?.logging?.serviceName ?? '@intflows/genkit-guard';
|
|
262
|
+
const levelRank = {
|
|
263
|
+
debug: 10,
|
|
264
|
+
info: 20,
|
|
265
|
+
warn: 30,
|
|
266
|
+
error: 40,
|
|
267
|
+
};
|
|
268
|
+
const severityNumber = {
|
|
269
|
+
debug: 5,
|
|
270
|
+
info: 9,
|
|
271
|
+
warn: 13,
|
|
272
|
+
error: 17,
|
|
273
|
+
};
|
|
274
|
+
return (severity, eventName, body, attributes = {}) => {
|
|
275
|
+
if (!enabled || levelRank[severity] < levelRank[minimumLevel]) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const record = {
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
severityText: severity.toUpperCase(),
|
|
281
|
+
severityNumber: severityNumber[severity],
|
|
282
|
+
body,
|
|
283
|
+
resource: {
|
|
284
|
+
attributes: {
|
|
285
|
+
'service.name': serviceName,
|
|
286
|
+
},
|
|
53
287
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
89
|
-
};
|
|
90
|
-
// ----------------------------------------------------------------------
|
|
91
|
-
// 5. Transform the entire response object in-place to unmask all strings
|
|
92
|
-
// ----------------------------------------------------------------------
|
|
93
|
-
transform(res);
|
|
94
|
-
console.log("[PII Guard] Deep unmasking complete across all candidates and custom fields.");
|
|
288
|
+
attributes: {
|
|
289
|
+
'event.name': eventName,
|
|
290
|
+
'code.namespace': 'genkit-guard',
|
|
291
|
+
...attributes,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
const line = JSON.stringify(record);
|
|
295
|
+
if (severity === 'error') {
|
|
296
|
+
console.error(line);
|
|
297
|
+
}
|
|
298
|
+
else if (severity === 'warn') {
|
|
299
|
+
console.warn(line);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.log(line);
|
|
95
303
|
}
|
|
96
|
-
// ---------------------------------------------------------
|
|
97
|
-
// 6. Return the modified response with unmasked content
|
|
98
|
-
// ---------------------------------------------------------
|
|
99
|
-
return res;
|
|
100
304
|
};
|
|
101
305
|
}
|
|
102
|
-
// Helper to create a blocked response
|
|
103
306
|
function block(message, metadata) {
|
|
104
307
|
return {
|
|
105
308
|
finishReason: 'blocked',
|
|
106
309
|
output: {
|
|
107
|
-
type:
|
|
108
|
-
status:
|
|
109
|
-
message
|
|
310
|
+
type: 'error',
|
|
311
|
+
status: 'BLOCKED',
|
|
312
|
+
message,
|
|
110
313
|
},
|
|
111
|
-
metadata
|
|
314
|
+
metadata,
|
|
112
315
|
};
|
|
113
316
|
}
|
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/pii/tokenizer.d.ts
CHANGED
package/dist/pii/tokenizer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export class PiiTokenizer {
|
|
2
2
|
vault = new Map();
|
|
3
|
+
valueToToken = new Map();
|
|
3
4
|
counter = 0;
|
|
4
5
|
piiTypes = new Set();
|
|
5
6
|
createToken(type) {
|
|
@@ -8,8 +9,16 @@ export class PiiTokenizer {
|
|
|
8
9
|
mask(text, matches) {
|
|
9
10
|
let masked = text;
|
|
10
11
|
for (const match of matches) {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
if (!masked.includes(match.value)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const key = `${match.type}:${match.value}`;
|
|
16
|
+
let token = this.valueToToken.get(key);
|
|
17
|
+
if (!token) {
|
|
18
|
+
token = this.createToken(match.type);
|
|
19
|
+
this.valueToToken.set(key, token);
|
|
20
|
+
this.vault.set(token, match.value);
|
|
21
|
+
}
|
|
13
22
|
this.piiTypes.add(match.type.toLowerCase());
|
|
14
23
|
masked = masked.split(match.value).join(token);
|
|
15
24
|
}
|
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();
|
|
@@ -20,21 +21,55 @@ export class ModelSingleton {
|
|
|
20
21
|
env.localModelPath = modelPath;
|
|
21
22
|
// Allow remote download if missing
|
|
22
23
|
env.allowRemoteModels = true;
|
|
23
|
-
console.log(
|
|
24
|
+
console.log(JSON.stringify({
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
severityText: 'INFO',
|
|
27
|
+
severityNumber: 9,
|
|
28
|
+
body: 'Using guard model directory',
|
|
29
|
+
resource: {
|
|
30
|
+
attributes: {
|
|
31
|
+
'service.name': '@intflows/genkit-guard',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
attributes: {
|
|
35
|
+
'event.name': 'guard.models.directory',
|
|
36
|
+
'code.namespace': 'genkit-guard',
|
|
37
|
+
modelPath,
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
24
40
|
}
|
|
25
|
-
static async getExtractor() {
|
|
26
|
-
if (!this.
|
|
41
|
+
static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
|
|
42
|
+
if (!this.extractors.has(modelName)) {
|
|
27
43
|
this.init();
|
|
28
|
-
|
|
44
|
+
const inst = await pipeline('feature-extraction', modelName);
|
|
45
|
+
this.extractors.set(modelName, inst);
|
|
29
46
|
}
|
|
30
|
-
return this.
|
|
47
|
+
return this.extractors.get(modelName);
|
|
31
48
|
}
|
|
32
|
-
static async getNER() {
|
|
33
|
-
if (!this.
|
|
49
|
+
static async getNER(modelName = 'Xenova/bert-base-NER') {
|
|
50
|
+
if (!this.nerClassifiers.has(modelName)) {
|
|
34
51
|
this.init();
|
|
35
|
-
|
|
36
|
-
this.
|
|
52
|
+
const inst = await pipeline('token-classification', modelName);
|
|
53
|
+
this.nerClassifiers.set(modelName, inst);
|
|
37
54
|
}
|
|
38
|
-
return this.
|
|
55
|
+
return this.nerClassifiers.get(modelName);
|
|
56
|
+
}
|
|
57
|
+
static async getPIIClassifier(modelName = 'openai/privacy-filter') {
|
|
58
|
+
if (!this.textClassifiers.has(modelName)) {
|
|
59
|
+
this.init();
|
|
60
|
+
const inst = await pipeline('text-classification', modelName);
|
|
61
|
+
this.textClassifiers.set(modelName, inst);
|
|
62
|
+
}
|
|
63
|
+
return this.textClassifiers.get(modelName);
|
|
64
|
+
}
|
|
65
|
+
static async preload(models) {
|
|
66
|
+
const tasks = [];
|
|
67
|
+
if (models?.extractor)
|
|
68
|
+
tasks.push(this.getExtractor(models.extractor));
|
|
69
|
+
if (models?.ner)
|
|
70
|
+
tasks.push(this.getNER(models.ner));
|
|
71
|
+
if (models?.pii)
|
|
72
|
+
tasks.push(this.getPIIClassifier(models.pii));
|
|
73
|
+
await Promise.all(tasks);
|
|
39
74
|
}
|
|
40
75
|
}
|
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-alpha.1",
|
|
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.39.0"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
48
|
+
"@types/node": "^25.8.0",
|
|
49
|
+
"genkit": "^1.39.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
|
}
|