@intflows/genkit-guard 0.0.14 → 0.1.0
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 +194 -20
- package/dist/core/audit.d.ts +5 -0
- package/dist/core/audit.js +23 -0
- package/dist/core/decision-storage.d.ts +50 -0
- package/dist/core/decision-storage.js +84 -0
- package/dist/core/decision.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -4
- package/dist/middleware/middleware.d.ts +102 -6
- package/dist/middleware/middleware.js +23 -14
- package/dist/pii/detector.d.ts +5 -5
- package/dist/pii/detector.js +27 -16
- package/dist/util/fallback.d.ts +8 -0
- package/dist/util/fallback.js +32 -0
- package/package.json +4 -3
- package/scripts/publish-wiki.js +1 -1
- package/scripts/test-release2.js +208 -0
- package/scripts/test-types.ts +11 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GuardDecisionStore } from '../core/decision-storage.js';
|
|
1
2
|
import { type GuardDecision, type ToolGuardConfig } from '../core/decision.js';
|
|
2
3
|
import { type PiiLabelMappings } from '../guard.config.js';
|
|
3
4
|
import { z } from 'genkit';
|
|
@@ -17,6 +18,11 @@ export type GuardConfig = {
|
|
|
17
18
|
reversible?: boolean;
|
|
18
19
|
model?: string;
|
|
19
20
|
mode?: 'ner' | 'classifier';
|
|
21
|
+
fallback?: {
|
|
22
|
+
model: string;
|
|
23
|
+
mode?: 'ner' | 'classifier';
|
|
24
|
+
labelMappings?: PiiLabelMappings;
|
|
25
|
+
};
|
|
20
26
|
labelMappings?: PiiLabelMappings;
|
|
21
27
|
vault?: {
|
|
22
28
|
storage?: PiiVaultStorage;
|
|
@@ -29,9 +35,12 @@ export type GuardConfig = {
|
|
|
29
35
|
serviceName?: string;
|
|
30
36
|
/** Awaited audit callback, independent of console logging level/enabled. Failure stops execution. */
|
|
31
37
|
onDecision?: (decision: GuardDecision) => void | Promise<void>;
|
|
38
|
+
/** Durable audit delivery. Failure stops execution; called before onDecision. */
|
|
39
|
+
store?: GuardDecisionStore;
|
|
32
40
|
};
|
|
33
41
|
models?: {
|
|
34
42
|
extractor?: string;
|
|
43
|
+
extractorFallback?: string;
|
|
35
44
|
};
|
|
36
45
|
[key: string]: any;
|
|
37
46
|
};
|
|
@@ -83,6 +92,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
83
92
|
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
84
93
|
model: z.ZodOptional<z.ZodString>;
|
|
85
94
|
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
95
|
+
fallback: z.ZodOptional<z.ZodObject<{
|
|
96
|
+
model: z.ZodString;
|
|
97
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
98
|
+
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
99
|
+
}, "strip", z.ZodTypeAny, {
|
|
100
|
+
model: string;
|
|
101
|
+
mode?: "ner" | "classifier" | undefined;
|
|
102
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
103
|
+
}, {
|
|
104
|
+
model: string;
|
|
105
|
+
mode?: "ner" | "classifier" | undefined;
|
|
106
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
107
|
+
}>>;
|
|
86
108
|
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
87
109
|
vault: z.ZodOptional<z.ZodObject<{
|
|
88
110
|
storage: z.ZodOptional<z.ZodAny>;
|
|
@@ -96,18 +118,28 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
96
118
|
}>>;
|
|
97
119
|
}, "strip", z.ZodTypeAny, {
|
|
98
120
|
model?: string | undefined;
|
|
99
|
-
|
|
121
|
+
fallback?: {
|
|
122
|
+
model: string;
|
|
123
|
+
mode?: "ner" | "classifier" | undefined;
|
|
124
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
125
|
+
} | undefined;
|
|
100
126
|
mode?: "ner" | "classifier" | undefined;
|
|
101
127
|
reversible?: boolean | undefined;
|
|
128
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
102
129
|
vault?: {
|
|
103
130
|
storage?: any;
|
|
104
131
|
scopeId?: any;
|
|
105
132
|
} | undefined;
|
|
106
133
|
}, {
|
|
107
134
|
model?: string | undefined;
|
|
108
|
-
|
|
135
|
+
fallback?: {
|
|
136
|
+
model: string;
|
|
137
|
+
mode?: "ner" | "classifier" | undefined;
|
|
138
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
139
|
+
} | undefined;
|
|
109
140
|
mode?: "ner" | "classifier" | undefined;
|
|
110
141
|
reversible?: boolean | undefined;
|
|
142
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
111
143
|
vault?: {
|
|
112
144
|
storage?: any;
|
|
113
145
|
scopeId?: any;
|
|
@@ -118,23 +150,29 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
118
150
|
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
119
151
|
serviceName: z.ZodOptional<z.ZodString>;
|
|
120
152
|
onDecision: z.ZodOptional<z.ZodAny>;
|
|
153
|
+
store: z.ZodOptional<z.ZodAny>;
|
|
121
154
|
}, "strip", z.ZodTypeAny, {
|
|
122
155
|
enabled?: boolean | undefined;
|
|
123
156
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
124
157
|
serviceName?: string | undefined;
|
|
125
158
|
onDecision?: any;
|
|
159
|
+
store?: any;
|
|
126
160
|
}, {
|
|
127
161
|
enabled?: boolean | undefined;
|
|
128
162
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
129
163
|
serviceName?: string | undefined;
|
|
130
164
|
onDecision?: any;
|
|
165
|
+
store?: any;
|
|
131
166
|
}>>;
|
|
132
167
|
models: z.ZodOptional<z.ZodObject<{
|
|
133
168
|
extractor: z.ZodOptional<z.ZodString>;
|
|
169
|
+
extractorFallback: z.ZodOptional<z.ZodString>;
|
|
134
170
|
}, "strip", z.ZodTypeAny, {
|
|
135
171
|
extractor?: string | undefined;
|
|
172
|
+
extractorFallback?: string | undefined;
|
|
136
173
|
}, {
|
|
137
174
|
extractor?: string | undefined;
|
|
175
|
+
extractorFallback?: string | undefined;
|
|
138
176
|
}>>;
|
|
139
177
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
140
178
|
policyVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -183,6 +221,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
183
221
|
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
184
222
|
model: z.ZodOptional<z.ZodString>;
|
|
185
223
|
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
224
|
+
fallback: z.ZodOptional<z.ZodObject<{
|
|
225
|
+
model: z.ZodString;
|
|
226
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
227
|
+
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
228
|
+
}, "strip", z.ZodTypeAny, {
|
|
229
|
+
model: string;
|
|
230
|
+
mode?: "ner" | "classifier" | undefined;
|
|
231
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
232
|
+
}, {
|
|
233
|
+
model: string;
|
|
234
|
+
mode?: "ner" | "classifier" | undefined;
|
|
235
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
236
|
+
}>>;
|
|
186
237
|
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
187
238
|
vault: z.ZodOptional<z.ZodObject<{
|
|
188
239
|
storage: z.ZodOptional<z.ZodAny>;
|
|
@@ -196,18 +247,28 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
196
247
|
}>>;
|
|
197
248
|
}, "strip", z.ZodTypeAny, {
|
|
198
249
|
model?: string | undefined;
|
|
199
|
-
|
|
250
|
+
fallback?: {
|
|
251
|
+
model: string;
|
|
252
|
+
mode?: "ner" | "classifier" | undefined;
|
|
253
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
254
|
+
} | undefined;
|
|
200
255
|
mode?: "ner" | "classifier" | undefined;
|
|
201
256
|
reversible?: boolean | undefined;
|
|
257
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
202
258
|
vault?: {
|
|
203
259
|
storage?: any;
|
|
204
260
|
scopeId?: any;
|
|
205
261
|
} | undefined;
|
|
206
262
|
}, {
|
|
207
263
|
model?: string | undefined;
|
|
208
|
-
|
|
264
|
+
fallback?: {
|
|
265
|
+
model: string;
|
|
266
|
+
mode?: "ner" | "classifier" | undefined;
|
|
267
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
268
|
+
} | undefined;
|
|
209
269
|
mode?: "ner" | "classifier" | undefined;
|
|
210
270
|
reversible?: boolean | undefined;
|
|
271
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
211
272
|
vault?: {
|
|
212
273
|
storage?: any;
|
|
213
274
|
scopeId?: any;
|
|
@@ -218,23 +279,29 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
218
279
|
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
219
280
|
serviceName: z.ZodOptional<z.ZodString>;
|
|
220
281
|
onDecision: z.ZodOptional<z.ZodAny>;
|
|
282
|
+
store: z.ZodOptional<z.ZodAny>;
|
|
221
283
|
}, "strip", z.ZodTypeAny, {
|
|
222
284
|
enabled?: boolean | undefined;
|
|
223
285
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
224
286
|
serviceName?: string | undefined;
|
|
225
287
|
onDecision?: any;
|
|
288
|
+
store?: any;
|
|
226
289
|
}, {
|
|
227
290
|
enabled?: boolean | undefined;
|
|
228
291
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
229
292
|
serviceName?: string | undefined;
|
|
230
293
|
onDecision?: any;
|
|
294
|
+
store?: any;
|
|
231
295
|
}>>;
|
|
232
296
|
models: z.ZodOptional<z.ZodObject<{
|
|
233
297
|
extractor: z.ZodOptional<z.ZodString>;
|
|
298
|
+
extractorFallback: z.ZodOptional<z.ZodString>;
|
|
234
299
|
}, "strip", z.ZodTypeAny, {
|
|
235
300
|
extractor?: string | undefined;
|
|
301
|
+
extractorFallback?: string | undefined;
|
|
236
302
|
}, {
|
|
237
303
|
extractor?: string | undefined;
|
|
304
|
+
extractorFallback?: string | undefined;
|
|
238
305
|
}>>;
|
|
239
306
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
240
307
|
policyVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -283,6 +350,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
283
350
|
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
284
351
|
model: z.ZodOptional<z.ZodString>;
|
|
285
352
|
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
353
|
+
fallback: z.ZodOptional<z.ZodObject<{
|
|
354
|
+
model: z.ZodString;
|
|
355
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
356
|
+
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
357
|
+
}, "strip", z.ZodTypeAny, {
|
|
358
|
+
model: string;
|
|
359
|
+
mode?: "ner" | "classifier" | undefined;
|
|
360
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
361
|
+
}, {
|
|
362
|
+
model: string;
|
|
363
|
+
mode?: "ner" | "classifier" | undefined;
|
|
364
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
365
|
+
}>>;
|
|
286
366
|
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
287
367
|
vault: z.ZodOptional<z.ZodObject<{
|
|
288
368
|
storage: z.ZodOptional<z.ZodAny>;
|
|
@@ -296,18 +376,28 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
296
376
|
}>>;
|
|
297
377
|
}, "strip", z.ZodTypeAny, {
|
|
298
378
|
model?: string | undefined;
|
|
299
|
-
|
|
379
|
+
fallback?: {
|
|
380
|
+
model: string;
|
|
381
|
+
mode?: "ner" | "classifier" | undefined;
|
|
382
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
383
|
+
} | undefined;
|
|
300
384
|
mode?: "ner" | "classifier" | undefined;
|
|
301
385
|
reversible?: boolean | undefined;
|
|
386
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
302
387
|
vault?: {
|
|
303
388
|
storage?: any;
|
|
304
389
|
scopeId?: any;
|
|
305
390
|
} | undefined;
|
|
306
391
|
}, {
|
|
307
392
|
model?: string | undefined;
|
|
308
|
-
|
|
393
|
+
fallback?: {
|
|
394
|
+
model: string;
|
|
395
|
+
mode?: "ner" | "classifier" | undefined;
|
|
396
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
397
|
+
} | undefined;
|
|
309
398
|
mode?: "ner" | "classifier" | undefined;
|
|
310
399
|
reversible?: boolean | undefined;
|
|
400
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
311
401
|
vault?: {
|
|
312
402
|
storage?: any;
|
|
313
403
|
scopeId?: any;
|
|
@@ -318,23 +408,29 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
318
408
|
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
319
409
|
serviceName: z.ZodOptional<z.ZodString>;
|
|
320
410
|
onDecision: z.ZodOptional<z.ZodAny>;
|
|
411
|
+
store: z.ZodOptional<z.ZodAny>;
|
|
321
412
|
}, "strip", z.ZodTypeAny, {
|
|
322
413
|
enabled?: boolean | undefined;
|
|
323
414
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
324
415
|
serviceName?: string | undefined;
|
|
325
416
|
onDecision?: any;
|
|
417
|
+
store?: any;
|
|
326
418
|
}, {
|
|
327
419
|
enabled?: boolean | undefined;
|
|
328
420
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
329
421
|
serviceName?: string | undefined;
|
|
330
422
|
onDecision?: any;
|
|
423
|
+
store?: any;
|
|
331
424
|
}>>;
|
|
332
425
|
models: z.ZodOptional<z.ZodObject<{
|
|
333
426
|
extractor: z.ZodOptional<z.ZodString>;
|
|
427
|
+
extractorFallback: z.ZodOptional<z.ZodString>;
|
|
334
428
|
}, "strip", z.ZodTypeAny, {
|
|
335
429
|
extractor?: string | undefined;
|
|
430
|
+
extractorFallback?: string | undefined;
|
|
336
431
|
}, {
|
|
337
432
|
extractor?: string | undefined;
|
|
433
|
+
extractorFallback?: string | undefined;
|
|
338
434
|
}>>;
|
|
339
435
|
}, z.ZodTypeAny, "passthrough">>, void>;
|
|
340
436
|
export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { publishDecision } from '../core/audit.js';
|
|
2
|
+
import { runGuardModel } from '../util/fallback.js';
|
|
2
3
|
import { GuardToolError } from '../core/decision.js';
|
|
3
4
|
import { resolveGuardModels } from '../guard.config.js';
|
|
4
5
|
import { generateMiddleware, z } from 'genkit';
|
|
@@ -27,6 +28,10 @@ const guardConfigSchema = z.object({
|
|
|
27
28
|
reversible: z.boolean().optional(),
|
|
28
29
|
model: z.string().optional(),
|
|
29
30
|
mode: z.enum(['ner', 'classifier']).optional(),
|
|
31
|
+
fallback: z.object({
|
|
32
|
+
model: z.string(), mode: z.enum(['ner', 'classifier']).optional(),
|
|
33
|
+
labelMappings: z.record(z.string(), z.string().regex(/^[A-Z_]+$/).nullable()).optional(),
|
|
34
|
+
}).optional(),
|
|
30
35
|
labelMappings: z.record(z.string(), z.string().regex(/^[A-Z_]+$/).nullable()).optional(),
|
|
31
36
|
vault: z.object({
|
|
32
37
|
storage: z.any().optional(),
|
|
@@ -38,9 +43,11 @@ const guardConfigSchema = z.object({
|
|
|
38
43
|
level: z.enum(['debug', 'info', 'warn', 'error']).optional(),
|
|
39
44
|
serviceName: z.string().optional(),
|
|
40
45
|
onDecision: z.any().optional(),
|
|
46
|
+
store: z.any().optional(),
|
|
41
47
|
}).optional(),
|
|
42
48
|
models: z.object({
|
|
43
49
|
extractor: z.string().optional(),
|
|
50
|
+
extractorFallback: z.string().optional(),
|
|
44
51
|
}).optional(),
|
|
45
52
|
}).passthrough();
|
|
46
53
|
export const guardMiddleware = generateMiddleware({
|
|
@@ -79,16 +86,7 @@ export const guardAction = guard;
|
|
|
79
86
|
function createGuardHooks(config) {
|
|
80
87
|
const logger = createLogger(config);
|
|
81
88
|
const models = resolveGuardModels(config);
|
|
82
|
-
const decide =
|
|
83
|
-
const decision = Object.freeze({
|
|
84
|
-
schemaVersion: '1', decisionId: randomUUID(), timestamp: new Date().toISOString(),
|
|
85
|
-
policyVersion: config?.policyVersion ?? 'unversioned',
|
|
86
|
-
latencyMs: Math.max(0, performance.now() - start), ...fields,
|
|
87
|
-
});
|
|
88
|
-
logger(decision.action === 'block' || decision.action === 'approval-required' ? 'warn' : 'info', 'guard.decision', 'Guard policy decision', { decision });
|
|
89
|
-
await config?.logging?.onDecision?.(decision);
|
|
90
|
-
return decision;
|
|
91
|
-
};
|
|
89
|
+
const decide = (start, fields) => publishDecision(config, start, fields);
|
|
92
90
|
return {
|
|
93
91
|
model: async (req, ctx, next) => {
|
|
94
92
|
const started = performance.now();
|
|
@@ -107,7 +105,8 @@ function createGuardHooks(config) {
|
|
|
107
105
|
await decide(started, { guard: 'injection', action: 'allow', reasonCode: 'INJECTION_CLEAR' });
|
|
108
106
|
const intentStarted = performance.now();
|
|
109
107
|
logger('info', 'guard.intent.analysis.start', 'Analyzing request intent');
|
|
110
|
-
const
|
|
108
|
+
const analyze = (model) => analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7, model);
|
|
109
|
+
const intentResult = await runGuardModel(config, 'intent', () => analyze(models.extractor), config?.models?.extractorFallback ? () => analyze(config.models.extractorFallback) : undefined);
|
|
111
110
|
logger('info', 'guard.intent.analysis.complete', 'Intent analysis completed', {
|
|
112
111
|
intent: intentResult.intent,
|
|
113
112
|
score: roundScore(intentResult.score),
|
|
@@ -143,7 +142,7 @@ function createGuardHooks(config) {
|
|
|
143
142
|
piiDetected: piiMatches.length > 0,
|
|
144
143
|
piiMatchCount: piiMatches.length,
|
|
145
144
|
piiTypes,
|
|
146
|
-
piiMode:
|
|
145
|
+
piiMode: piiResponse.effectiveMode ?? models.mode,
|
|
147
146
|
classifierOutputPresent: Boolean(piiResponse.classifier),
|
|
148
147
|
});
|
|
149
148
|
req.metadata = {
|
|
@@ -156,6 +155,9 @@ function createGuardHooks(config) {
|
|
|
156
155
|
piiModel: config?.pii?.model,
|
|
157
156
|
piiMode: config?.pii?.mode,
|
|
158
157
|
piiClassifierOutput: piiResponse.classifier,
|
|
158
|
+
piiEffectiveModel: piiResponse.effectiveModel,
|
|
159
|
+
piiEffectiveMode: piiResponse.effectiveMode,
|
|
160
|
+
piiUsedFallback: piiResponse.usedFallback,
|
|
159
161
|
};
|
|
160
162
|
const res = await next(req, ctx);
|
|
161
163
|
const unmaskedResponse = await unmaskObject(res, getGuardState(ctx).tokenizers);
|
|
@@ -212,6 +214,9 @@ function createGuardHooks(config) {
|
|
|
212
214
|
piiDetected: piiMatches.length > 0,
|
|
213
215
|
piiTypes,
|
|
214
216
|
piiMatchCount: piiMatches.length,
|
|
217
|
+
piiEffectiveModel: piiResponse.effectiveModel,
|
|
218
|
+
piiEffectiveMode: piiResponse.effectiveMode,
|
|
219
|
+
piiUsedFallback: piiResponse.usedFallback,
|
|
215
220
|
};
|
|
216
221
|
logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.tool.pii.checked', 'Tool request PII scan completed', {
|
|
217
222
|
toolName,
|
|
@@ -326,6 +331,9 @@ async function scanPII(text, config) {
|
|
|
326
331
|
return {
|
|
327
332
|
matches: [],
|
|
328
333
|
classifier: undefined,
|
|
334
|
+
effectiveModel: undefined,
|
|
335
|
+
effectiveMode: undefined,
|
|
336
|
+
usedFallback: false,
|
|
329
337
|
};
|
|
330
338
|
}
|
|
331
339
|
const models = resolveGuardModels(config);
|
|
@@ -333,7 +341,8 @@ async function scanPII(text, config) {
|
|
|
333
341
|
model: models.pii,
|
|
334
342
|
mode: models.mode,
|
|
335
343
|
labelMappings: config?.pii?.labelMappings,
|
|
336
|
-
|
|
344
|
+
fallback: config?.pii?.fallback,
|
|
345
|
+
}, config);
|
|
337
346
|
}
|
|
338
347
|
function getGuardState(ctx = {}) {
|
|
339
348
|
ctx.context = ctx.context || {};
|
package/dist/pii/detector.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GuardConfig } from '../middleware/middleware.js';
|
|
1
2
|
import type { PiiLabelMappings } from '../guard.config.js';
|
|
2
3
|
export type PiiMatch = {
|
|
3
4
|
type: string;
|
|
@@ -11,12 +12,11 @@ export type PrivacyFilterSpan = {
|
|
|
11
12
|
end?: number;
|
|
12
13
|
score?: number;
|
|
13
14
|
};
|
|
14
|
-
export declare function detectPII(text: string, opts?: {
|
|
15
|
-
model?: string;
|
|
16
|
-
mode?: 'ner' | 'classifier';
|
|
17
|
-
labelMappings?: PiiLabelMappings;
|
|
18
|
-
}): Promise<{
|
|
15
|
+
export declare function detectPII(text: string, opts?: GuardConfig['pii'], config?: GuardConfig): Promise<{
|
|
19
16
|
matches: PiiMatch[];
|
|
20
17
|
classifier: any;
|
|
18
|
+
effectiveModel: string;
|
|
19
|
+
effectiveMode: "ner" | "classifier";
|
|
20
|
+
usedFallback: boolean;
|
|
21
21
|
}>;
|
|
22
22
|
export declare function privacyFilterOutputToMatches(text: string, output: unknown, labelMappings?: PiiLabelMappings): PiiMatch[];
|
package/dist/pii/detector.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runGuardModel } from '../util/fallback.js';
|
|
1
2
|
import { ModelSingleton } from '../util/singleton.js';
|
|
2
3
|
const PRIVACY_FILTER_TYPE_MAP = {
|
|
3
4
|
account_number: 'ACCOUNT_NUMBER',
|
|
@@ -25,7 +26,7 @@ const REGEX_RULES = [
|
|
|
25
26
|
// CREDIT CARD (keep your existing one if needed)
|
|
26
27
|
{ type: 'CREDIT_CARD', pattern: /\b(?:\d[ -]*?){13,16}\b/g }
|
|
27
28
|
];
|
|
28
|
-
export async function detectPII(text, opts) {
|
|
29
|
+
export async function detectPII(text, opts, config) {
|
|
29
30
|
const mode = opts?.mode ?? 'ner';
|
|
30
31
|
const model = opts?.model;
|
|
31
32
|
const results = [];
|
|
@@ -34,16 +35,27 @@ export async function detectPII(text, opts) {
|
|
|
34
35
|
const matches = text.match(rule.pattern) || [];
|
|
35
36
|
matches.forEach(m => results.push({ type: rule.type, value: m }));
|
|
36
37
|
}
|
|
37
|
-
//
|
|
38
|
+
// Only model loading/inference is retried. Label mapping errors are not model failures.
|
|
39
|
+
const infer = async (selected, usedFallback = false) => {
|
|
40
|
+
const selectedMode = selected.mode ?? mode;
|
|
41
|
+
const pipeline = selectedMode === 'ner'
|
|
42
|
+
? await ModelSingleton.getNER(selected.model)
|
|
43
|
+
: await ModelSingleton.getPIIClassifier(selected.model);
|
|
44
|
+
const output = selectedMode === 'ner' ? await pipeline(text)
|
|
45
|
+
: await pipeline(text, { aggregation_strategy: 'simple' });
|
|
46
|
+
if (!Array.isArray(output))
|
|
47
|
+
throw new Error('Invalid PII model output');
|
|
48
|
+
return { output, mode: selectedMode, labelMappings: selected.labelMappings, usedFallback,
|
|
49
|
+
model: selected.model ?? (selectedMode === 'ner' ? 'Xenova/bert-base-NER' : 'openai/privacy-filter') };
|
|
50
|
+
};
|
|
51
|
+
const selected = await runGuardModel(config, 'pii', () => infer({ model, mode, labelMappings: opts?.labelMappings }), opts?.fallback ? () => infer(opts.fallback, true) : undefined);
|
|
38
52
|
let classifierOutput = undefined;
|
|
39
|
-
if (mode === 'ner') {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
for (const e of entities) {
|
|
43
|
-
const mappedType = mappedLabel(e.entity_group ?? e.entity, opts?.labelMappings);
|
|
53
|
+
if (selected.mode === 'ner') {
|
|
54
|
+
for (const e of selected.output) {
|
|
55
|
+
const mappedType = mappedLabel(e.entity_group ?? e.entity, selected.labelMappings);
|
|
44
56
|
if (mappedType !== undefined) {
|
|
45
57
|
if (mappedType)
|
|
46
|
-
results.push(...privacyFilterOutputToMatches(text, [e],
|
|
58
|
+
results.push(...privacyFilterOutputToMatches(text, [e], selected.labelMappings));
|
|
47
59
|
}
|
|
48
60
|
else if (e.entity && e.entity.includes('PER')) {
|
|
49
61
|
results.push({ type: 'NAME', value: (e.word || '').replace(/##/g, '') });
|
|
@@ -51,19 +63,18 @@ export async function detectPII(text, opts) {
|
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
classifierOutput = await cls(text, { aggregation_strategy: 'simple' });
|
|
58
|
-
for (const match of privacyFilterOutputToMatches(text, classifierOutput, opts?.labelMappings)) {
|
|
59
|
-
if (!results.some((existing) => existing.value === match.value)) {
|
|
66
|
+
classifierOutput = selected.output;
|
|
67
|
+
for (const match of privacyFilterOutputToMatches(text, classifierOutput, selected.labelMappings)) {
|
|
68
|
+
if (!results.some((existing) => existing.value === match.value))
|
|
60
69
|
results.push(match);
|
|
61
|
-
}
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
return {
|
|
65
73
|
matches: results,
|
|
66
|
-
classifier: classifierOutput
|
|
74
|
+
classifier: classifierOutput,
|
|
75
|
+
effectiveModel: selected.model,
|
|
76
|
+
effectiveMode: selected.mode,
|
|
77
|
+
usedFallback: selected.usedFallback,
|
|
67
78
|
};
|
|
68
79
|
}
|
|
69
80
|
export function privacyFilterOutputToMatches(text, output, labelMappings) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GuardConfig } from '../middleware/middleware.js';
|
|
2
|
+
export declare class GuardModelError extends Error {
|
|
3
|
+
readonly guard: 'intent' | 'pii';
|
|
4
|
+
readonly code = "MODEL_UNAVAILABLE";
|
|
5
|
+
constructor(guard: 'intent' | 'pii');
|
|
6
|
+
}
|
|
7
|
+
/** One fallback attempt per operation, only after a model operation throws. */
|
|
8
|
+
export declare function runGuardModel<T>(config: GuardConfig | undefined, guard: 'intent' | 'pii', primary: () => Promise<T>, fallback?: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { publishDecision } from '../core/audit.js';
|
|
2
|
+
export class GuardModelError extends Error {
|
|
3
|
+
guard;
|
|
4
|
+
code = 'MODEL_UNAVAILABLE';
|
|
5
|
+
constructor(guard) {
|
|
6
|
+
// Do not attach underlying errors: inference errors can contain request content.
|
|
7
|
+
super(`Primary and fallback ${guard} models failed`);
|
|
8
|
+
this.guard = guard;
|
|
9
|
+
this.name = 'GuardModelError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** One fallback attempt per operation, only after a model operation throws. */
|
|
13
|
+
export async function runGuardModel(config, guard, primary, fallback) {
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
try {
|
|
16
|
+
return await primary();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!fallback)
|
|
20
|
+
throw error; // Preserve existing failure behavior without opt-in.
|
|
21
|
+
}
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
result = await fallback();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
await publishDecision(config, start, { guard, action: 'block', reasonCode: 'MODEL_UNAVAILABLE' });
|
|
28
|
+
throw new GuardModelError(guard);
|
|
29
|
+
}
|
|
30
|
+
await publishDecision(config, start, { guard, action: 'allow', reasonCode: 'MODEL_FALLBACK_USED' });
|
|
31
|
+
return result;
|
|
32
|
+
}
|
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.1.0",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"prepare-models": "node scripts/download-model.js",
|
|
37
37
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
38
38
|
"build": "npm run clean && tsc",
|
|
39
|
-
"test": "npm run test:types && npm run test:privacy-filter && npm run test:storage && npm run test:concurrency && npm run test:config && npm run test:release1",
|
|
39
|
+
"test": "npm run test:types && npm run test:privacy-filter && npm run test:storage && npm run test:concurrency && npm run test:config && npm run test:release1 && npm run test:release2",
|
|
40
40
|
"test:types": "tsc --noEmit --ignoreConfig --module NodeNext --moduleResolution NodeNext --target ESNext --strict --skipLibCheck scripts/test-types.ts",
|
|
41
41
|
"test:privacy-filter": "npm run build && node scripts/test-privacy-filter.js",
|
|
42
42
|
"test:storage": "npm run build && node scripts/test-storage.js",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"prepublishOnly": "npm run build",
|
|
46
46
|
"test:config": "npm run build && node scripts/test-config.js",
|
|
47
47
|
"wiki:publish": "node scripts/publish-wiki.js",
|
|
48
|
-
"test:release1": "npm run build && node --test scripts/test-release1.js scripts/test-publish-wiki.js"
|
|
48
|
+
"test:release1": "npm run build && node --test scripts/test-release1.js scripts/test-publish-wiki.js",
|
|
49
|
+
"test:release2": "npm run build && node --test scripts/test-release2.js"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@huggingface/transformers": "^4.2.0",
|
package/scripts/publish-wiki.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
|
-
let source = join(root, 'docs/wiki-v0.0
|
|
10
|
+
let source = join(root, 'docs/wiki-v0.1.0');
|
|
11
11
|
let repository = 'https://github.com/IntFlows/genkit-guard.wiki.git';
|
|
12
12
|
let publish = false;
|
|
13
13
|
for (let i = 0; i < args.length; i++) {
|