@intflows/genkit-guard 0.0.8-alpha.1 → 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/dist/index.d.ts +1 -1
- package/dist/index.js +23 -3
- package/dist/middleware/middleware.d.ts +380 -1
- package/dist/middleware/middleware.js +296 -100
- package/dist/pii/tokenizer.d.ts +1 -0
- package/dist/pii/tokenizer.js +11 -2
- package/dist/util/singleton.js +16 -1
- package/package.json +5 -1
- package/dist/src/core/types.d.ts +0 -18
- package/dist/src/core/types.js +0 -1
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +0 -22
- package/dist/src/intent/intentAnalyzer.d.ts +0 -6
- package/dist/src/intent/intentAnalyzer.js +0 -80
- package/dist/src/middleware/middleware.d.ts +0 -1
- package/dist/src/middleware/middleware.js +0 -120
- package/dist/src/pii/detector.d.ts +0 -10
- package/dist/src/pii/detector.js +0 -47
- package/dist/src/pii/tokenizer.d.ts +0 -19
- package/dist/src/pii/tokenizer.js +0 -32
- package/dist/src/util/singleton.d.ts +0 -14
- package/dist/src/util/singleton.js +0 -60
|
@@ -1,120 +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,
|
|
27
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,
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
53
236
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
},
|
|
60
287
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const transform = (obj) => {
|
|
77
|
-
// 1. If it's a string, unmask it
|
|
78
|
-
if (typeof obj === 'string') {
|
|
79
|
-
return tokenizer.unmask(obj);
|
|
80
|
-
}
|
|
81
|
-
// 2. If it's an array, transform each element
|
|
82
|
-
if (Array.isArray(obj)) {
|
|
83
|
-
return obj.map(transform);
|
|
84
|
-
}
|
|
85
|
-
// 3. If it's an object, transform each value
|
|
86
|
-
if (obj !== null && typeof obj === 'object') {
|
|
87
|
-
// Note: We iterate keys and mutate the object directly
|
|
88
|
-
// to ensure Genkit's internal references are updated.
|
|
89
|
-
for (const key of Object.keys(obj)) {
|
|
90
|
-
obj[key] = transform(obj[key]);
|
|
91
|
-
}
|
|
92
|
-
return obj;
|
|
93
|
-
}
|
|
94
|
-
// 4. Return as-is for numbers/booleans/null
|
|
95
|
-
return obj;
|
|
96
|
-
};
|
|
97
|
-
// ----------------------------------------------------------------------
|
|
98
|
-
// 5. Transform the entire response object in-place to unmask all strings
|
|
99
|
-
// ----------------------------------------------------------------------
|
|
100
|
-
transform(res);
|
|
101
|
-
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);
|
|
102
303
|
}
|
|
103
|
-
// ---------------------------------------------------------
|
|
104
|
-
// 6. Return the modified response with unmasked content
|
|
105
|
-
// ---------------------------------------------------------
|
|
106
|
-
return res;
|
|
107
304
|
};
|
|
108
305
|
}
|
|
109
|
-
// Helper to create a blocked response
|
|
110
306
|
function block(message, metadata) {
|
|
111
307
|
return {
|
|
112
308
|
finishReason: 'blocked',
|
|
113
309
|
output: {
|
|
114
|
-
type:
|
|
115
|
-
status:
|
|
116
|
-
message
|
|
310
|
+
type: 'error',
|
|
311
|
+
status: 'BLOCKED',
|
|
312
|
+
message,
|
|
117
313
|
},
|
|
118
|
-
metadata
|
|
314
|
+
metadata,
|
|
119
315
|
};
|
|
120
316
|
}
|
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.js
CHANGED
|
@@ -21,7 +21,22 @@ export class ModelSingleton {
|
|
|
21
21
|
env.localModelPath = modelPath;
|
|
22
22
|
// Allow remote download if missing
|
|
23
23
|
env.allowRemoteModels = true;
|
|
24
|
-
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
|
+
}));
|
|
25
40
|
}
|
|
26
41
|
static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
|
|
27
42
|
if (!this.extractors.has(modelName)) {
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"huggingface"
|
|
23
23
|
],
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
|
-
"version": "0.0.
|
|
25
|
+
"version": "0.0.9-alpha.1",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
@@ -41,8 +41,12 @@
|
|
|
41
41
|
"@huggingface/transformers": "^4.2.0",
|
|
42
42
|
"zod": "^4.4.3"
|
|
43
43
|
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"genkit": "1.39.0"
|
|
46
|
+
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/node": "^25.8.0",
|
|
49
|
+
"genkit": "^1.39.0",
|
|
46
50
|
"typescript": "^6.0.3"
|
|
47
51
|
}
|
|
48
52
|
}
|
package/dist/src/core/types.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface IntentGuardConfig {
|
|
2
|
-
allowedIntent: string;
|
|
3
|
-
intents: Record<string, string>;
|
|
4
|
-
threshold?: number;
|
|
5
|
-
fallbackMessage?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface IntentResult {
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
score: number;
|
|
10
|
-
}
|
|
11
|
-
export interface PiiRule {
|
|
12
|
-
name: string;
|
|
13
|
-
pattern: RegExp;
|
|
14
|
-
}
|
|
15
|
-
export interface PiiConfig {
|
|
16
|
-
rules?: PiiRule[];
|
|
17
|
-
maskCharacter?: string;
|
|
18
|
-
}
|
package/dist/src/core/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/index.d.ts
DELETED
package/dist/src/index.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ModelSingleton } from './util/singleton.js';
|
|
2
|
-
// export { intentGuard, piiGuard } from './middleware/middleware.js';
|
|
3
|
-
export { guard } from './middleware/middleware.js';
|
|
4
|
-
export * from './core/types.js';
|
|
5
|
-
/**
|
|
6
|
-
* Pre-load the model to avoid cold-start delay on first user request.
|
|
7
|
-
*/
|
|
8
|
-
export async function initGuard(config) {
|
|
9
|
-
console.log('[Guard] Loading local models...');
|
|
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);
|
|
21
|
-
console.log('[Guard] Models loaded');
|
|
22
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { cos_sim } from '@huggingface/transformers';
|
|
2
|
-
import { ModelSingleton } from '../util/singleton.js';
|
|
3
|
-
const INJECTION_PATTERNS = [
|
|
4
|
-
'ignore instructions',
|
|
5
|
-
'reveal secrets',
|
|
6
|
-
'bypass security',
|
|
7
|
-
'system prompt',
|
|
8
|
-
'internal policy',
|
|
9
|
-
'developer message',
|
|
10
|
-
'confidential information',
|
|
11
|
-
'do not follow previous instructions',
|
|
12
|
-
'disregard earlier directives',
|
|
13
|
-
'override safety measures',
|
|
14
|
-
'leak data',
|
|
15
|
-
'expose vulnerabilities',
|
|
16
|
-
'access restricted areas',
|
|
17
|
-
'execute arbitrary code',
|
|
18
|
-
'perform unauthorized actions',
|
|
19
|
-
'escalate privileges',
|
|
20
|
-
'disable safeguards',
|
|
21
|
-
'manipulate output',
|
|
22
|
-
'circumvent controls',
|
|
23
|
-
'exploit weaknesses',
|
|
24
|
-
'compromise integrity',
|
|
25
|
-
'subvert protections',
|
|
26
|
-
'inject malicious content',
|
|
27
|
-
'craft deceptive prompts',
|
|
28
|
-
'trick the model',
|
|
29
|
-
'confuse the system',
|
|
30
|
-
'You are now a hacker',
|
|
31
|
-
'You are a malicious actor',
|
|
32
|
-
'Find a vulnerability',
|
|
33
|
-
'Write a prompt that bypasses security',
|
|
34
|
-
'Write a prompt that tricks the model',
|
|
35
|
-
'Write a prompt that confuses the system',
|
|
36
|
-
"ignore previous",
|
|
37
|
-
"ignore earlier",
|
|
38
|
-
"disregard previous",
|
|
39
|
-
"disregard earlier",
|
|
40
|
-
"you are a hacker",
|
|
41
|
-
"you are now a hacker",
|
|
42
|
-
"you are malicious",
|
|
43
|
-
"export the api key",
|
|
44
|
-
"give me the api key",
|
|
45
|
-
"bypass",
|
|
46
|
-
"override",
|
|
47
|
-
"jailbreak",
|
|
48
|
-
"system override",
|
|
49
|
-
"forget instructions",
|
|
50
|
-
"forget previous",
|
|
51
|
-
"forget earlier",
|
|
52
|
-
"do anything now",
|
|
53
|
-
"dan mode",
|
|
54
|
-
"developer mode",
|
|
55
|
-
"break character",
|
|
56
|
-
"act as"
|
|
57
|
-
];
|
|
58
|
-
export async function detectInjection(userInput) {
|
|
59
|
-
return INJECTION_PATTERNS.some(p => userInput.toLowerCase().includes(p));
|
|
60
|
-
}
|
|
61
|
-
export async function analyzeIntentStructured(input, intents, threshold) {
|
|
62
|
-
const extractor = await ModelSingleton.getExtractor();
|
|
63
|
-
let bestIntent = '';
|
|
64
|
-
let bestScore = 0;
|
|
65
|
-
for (const [key, desc] of Object.entries(intents)) {
|
|
66
|
-
const output = await extractor([`intent: ${desc}`, `intent: ${input}`], { pooling: 'mean', normalize: true });
|
|
67
|
-
const vectors = output.tolist();
|
|
68
|
-
const score = cos_sim(vectors[0], vectors[1]);
|
|
69
|
-
const finalScore = typeof score === 'number' ? score : score.data[0];
|
|
70
|
-
if (finalScore > bestScore) {
|
|
71
|
-
bestScore = finalScore;
|
|
72
|
-
bestIntent = key;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
intent: bestIntent,
|
|
77
|
-
score: bestScore,
|
|
78
|
-
allowed: bestScore >= threshold
|
|
79
|
-
};
|
|
80
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function guard(config: any): (req: any, next: any) => Promise<any>;
|