@intflows/genkit-guard 0.0.12 → 0.0.14
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 +174 -186
- package/dist/core/decision.d.ts +33 -0
- package/dist/core/decision.js +9 -0
- package/dist/guard.config.d.ts +10 -0
- package/dist/guard.config.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +8 -10
- package/dist/intent/intentAnalyzer.d.ts +1 -1
- package/dist/intent/intentAnalyzer.js +2 -2
- package/dist/middleware/middleware.d.ts +87 -7
- package/dist/middleware/middleware.js +93 -4
- package/dist/pii/detector.d.ts +16 -4
- package/dist/pii/detector.js +72 -3
- package/dist/util/singleton.d.ts +2 -1
- package/dist/util/singleton.js +8 -5
- package/package.json +9 -4
- package/scripts/publish-wiki.js +59 -0
- package/scripts/test-config.js +45 -0
- package/scripts/test-privacy-filter.js +116 -0
- package/scripts/test-publish-wiki.js +30 -0
- package/scripts/test-release1.js +83 -0
- package/scripts/test-types.ts +51 -21
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { type GuardDecision, type ToolGuardConfig } from '../core/decision.js';
|
|
2
|
+
import { type PiiLabelMappings } from '../guard.config.js';
|
|
1
3
|
import { z } from 'genkit';
|
|
2
4
|
import { type PiiVaultStorage } from '../pii/storage.js';
|
|
3
5
|
export type GuardConfig = {
|
|
6
|
+
policyVersion?: string;
|
|
7
|
+
tools?: ToolGuardConfig;
|
|
4
8
|
intent?: {
|
|
5
9
|
mode?: string;
|
|
6
10
|
allowedIntent?: string;
|
|
@@ -13,6 +17,7 @@ export type GuardConfig = {
|
|
|
13
17
|
reversible?: boolean;
|
|
14
18
|
model?: string;
|
|
15
19
|
mode?: 'ner' | 'classifier';
|
|
20
|
+
labelMappings?: PiiLabelMappings;
|
|
16
21
|
vault?: {
|
|
17
22
|
storage?: PiiVaultStorage;
|
|
18
23
|
scopeId?: string | ((req: any, ctx: any) => string | undefined);
|
|
@@ -22,6 +27,8 @@ export type GuardConfig = {
|
|
|
22
27
|
enabled?: boolean;
|
|
23
28
|
level?: LogSeverity;
|
|
24
29
|
serviceName?: string;
|
|
30
|
+
/** Awaited audit callback, independent of console logging level/enabled. Failure stops execution. */
|
|
31
|
+
onDecision?: (decision: GuardDecision) => void | Promise<void>;
|
|
25
32
|
};
|
|
26
33
|
models?: {
|
|
27
34
|
extractor?: string;
|
|
@@ -30,6 +37,20 @@ export type GuardConfig = {
|
|
|
30
37
|
};
|
|
31
38
|
type LogSeverity = 'debug' | 'info' | 'warn' | 'error';
|
|
32
39
|
export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodObject<{
|
|
40
|
+
policyVersion: z.ZodOptional<z.ZodString>;
|
|
41
|
+
tools: z.ZodOptional<z.ZodObject<{
|
|
42
|
+
defaultAction: z.ZodOptional<z.ZodEnum<["allow", "block", "redact", "approval-required"]>>;
|
|
43
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<["allow", "block", "redact", "approval-required"]>>>;
|
|
44
|
+
approve: z.ZodOptional<z.ZodAny>;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
defaultAction?: "allow" | "block" | "redact" | "approval-required" | undefined;
|
|
47
|
+
rules?: Record<string, "allow" | "block" | "redact" | "approval-required"> | undefined;
|
|
48
|
+
approve?: any;
|
|
49
|
+
}, {
|
|
50
|
+
defaultAction?: "allow" | "block" | "redact" | "approval-required" | undefined;
|
|
51
|
+
rules?: Record<string, "allow" | "block" | "redact" | "approval-required"> | undefined;
|
|
52
|
+
approve?: any;
|
|
53
|
+
}>>;
|
|
33
54
|
intent: z.ZodOptional<z.ZodObject<{
|
|
34
55
|
mode: z.ZodOptional<z.ZodString>;
|
|
35
56
|
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
@@ -62,6 +83,7 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
62
83
|
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
63
84
|
model: z.ZodOptional<z.ZodString>;
|
|
64
85
|
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
86
|
+
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
65
87
|
vault: z.ZodOptional<z.ZodObject<{
|
|
66
88
|
storage: z.ZodOptional<z.ZodAny>;
|
|
67
89
|
scopeId: z.ZodOptional<z.ZodAny>;
|
|
@@ -73,17 +95,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
73
95
|
scopeId?: any;
|
|
74
96
|
}>>;
|
|
75
97
|
}, "strip", z.ZodTypeAny, {
|
|
98
|
+
model?: string | undefined;
|
|
99
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
76
100
|
mode?: "ner" | "classifier" | undefined;
|
|
77
101
|
reversible?: boolean | undefined;
|
|
78
|
-
model?: string | undefined;
|
|
79
102
|
vault?: {
|
|
80
103
|
storage?: any;
|
|
81
104
|
scopeId?: any;
|
|
82
105
|
} | undefined;
|
|
83
106
|
}, {
|
|
107
|
+
model?: string | undefined;
|
|
108
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
84
109
|
mode?: "ner" | "classifier" | undefined;
|
|
85
110
|
reversible?: boolean | undefined;
|
|
86
|
-
model?: string | undefined;
|
|
87
111
|
vault?: {
|
|
88
112
|
storage?: any;
|
|
89
113
|
scopeId?: any;
|
|
@@ -93,14 +117,17 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
93
117
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
94
118
|
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
95
119
|
serviceName: z.ZodOptional<z.ZodString>;
|
|
120
|
+
onDecision: z.ZodOptional<z.ZodAny>;
|
|
96
121
|
}, "strip", z.ZodTypeAny, {
|
|
97
122
|
enabled?: boolean | undefined;
|
|
98
123
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
99
124
|
serviceName?: string | undefined;
|
|
125
|
+
onDecision?: any;
|
|
100
126
|
}, {
|
|
101
127
|
enabled?: boolean | undefined;
|
|
102
128
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
103
129
|
serviceName?: string | undefined;
|
|
130
|
+
onDecision?: any;
|
|
104
131
|
}>>;
|
|
105
132
|
models: z.ZodOptional<z.ZodObject<{
|
|
106
133
|
extractor: z.ZodOptional<z.ZodString>;
|
|
@@ -110,6 +137,20 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
110
137
|
extractor?: string | undefined;
|
|
111
138
|
}>>;
|
|
112
139
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
140
|
+
policyVersion: z.ZodOptional<z.ZodString>;
|
|
141
|
+
tools: z.ZodOptional<z.ZodObject<{
|
|
142
|
+
defaultAction: z.ZodOptional<z.ZodEnum<["allow", "block", "redact", "approval-required"]>>;
|
|
143
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<["allow", "block", "redact", "approval-required"]>>>;
|
|
144
|
+
approve: z.ZodOptional<z.ZodAny>;
|
|
145
|
+
}, "strip", z.ZodTypeAny, {
|
|
146
|
+
defaultAction?: "allow" | "block" | "redact" | "approval-required" | undefined;
|
|
147
|
+
rules?: Record<string, "allow" | "block" | "redact" | "approval-required"> | undefined;
|
|
148
|
+
approve?: any;
|
|
149
|
+
}, {
|
|
150
|
+
defaultAction?: "allow" | "block" | "redact" | "approval-required" | undefined;
|
|
151
|
+
rules?: Record<string, "allow" | "block" | "redact" | "approval-required"> | undefined;
|
|
152
|
+
approve?: any;
|
|
153
|
+
}>>;
|
|
113
154
|
intent: z.ZodOptional<z.ZodObject<{
|
|
114
155
|
mode: z.ZodOptional<z.ZodString>;
|
|
115
156
|
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
@@ -142,6 +183,7 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
142
183
|
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
143
184
|
model: z.ZodOptional<z.ZodString>;
|
|
144
185
|
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
186
|
+
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
145
187
|
vault: z.ZodOptional<z.ZodObject<{
|
|
146
188
|
storage: z.ZodOptional<z.ZodAny>;
|
|
147
189
|
scopeId: z.ZodOptional<z.ZodAny>;
|
|
@@ -153,17 +195,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
153
195
|
scopeId?: any;
|
|
154
196
|
}>>;
|
|
155
197
|
}, "strip", z.ZodTypeAny, {
|
|
198
|
+
model?: string | undefined;
|
|
199
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
156
200
|
mode?: "ner" | "classifier" | undefined;
|
|
157
201
|
reversible?: boolean | undefined;
|
|
158
|
-
model?: string | undefined;
|
|
159
202
|
vault?: {
|
|
160
203
|
storage?: any;
|
|
161
204
|
scopeId?: any;
|
|
162
205
|
} | undefined;
|
|
163
206
|
}, {
|
|
207
|
+
model?: string | undefined;
|
|
208
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
164
209
|
mode?: "ner" | "classifier" | undefined;
|
|
165
210
|
reversible?: boolean | undefined;
|
|
166
|
-
model?: string | undefined;
|
|
167
211
|
vault?: {
|
|
168
212
|
storage?: any;
|
|
169
213
|
scopeId?: any;
|
|
@@ -173,14 +217,17 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
173
217
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
174
218
|
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
175
219
|
serviceName: z.ZodOptional<z.ZodString>;
|
|
220
|
+
onDecision: z.ZodOptional<z.ZodAny>;
|
|
176
221
|
}, "strip", z.ZodTypeAny, {
|
|
177
222
|
enabled?: boolean | undefined;
|
|
178
223
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
179
224
|
serviceName?: string | undefined;
|
|
225
|
+
onDecision?: any;
|
|
180
226
|
}, {
|
|
181
227
|
enabled?: boolean | undefined;
|
|
182
228
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
183
229
|
serviceName?: string | undefined;
|
|
230
|
+
onDecision?: any;
|
|
184
231
|
}>>;
|
|
185
232
|
models: z.ZodOptional<z.ZodObject<{
|
|
186
233
|
extractor: z.ZodOptional<z.ZodString>;
|
|
@@ -190,6 +237,20 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
190
237
|
extractor?: string | undefined;
|
|
191
238
|
}>>;
|
|
192
239
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
240
|
+
policyVersion: z.ZodOptional<z.ZodString>;
|
|
241
|
+
tools: z.ZodOptional<z.ZodObject<{
|
|
242
|
+
defaultAction: z.ZodOptional<z.ZodEnum<["allow", "block", "redact", "approval-required"]>>;
|
|
243
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<["allow", "block", "redact", "approval-required"]>>>;
|
|
244
|
+
approve: z.ZodOptional<z.ZodAny>;
|
|
245
|
+
}, "strip", z.ZodTypeAny, {
|
|
246
|
+
defaultAction?: "allow" | "block" | "redact" | "approval-required" | undefined;
|
|
247
|
+
rules?: Record<string, "allow" | "block" | "redact" | "approval-required"> | undefined;
|
|
248
|
+
approve?: any;
|
|
249
|
+
}, {
|
|
250
|
+
defaultAction?: "allow" | "block" | "redact" | "approval-required" | undefined;
|
|
251
|
+
rules?: Record<string, "allow" | "block" | "redact" | "approval-required"> | undefined;
|
|
252
|
+
approve?: any;
|
|
253
|
+
}>>;
|
|
193
254
|
intent: z.ZodOptional<z.ZodObject<{
|
|
194
255
|
mode: z.ZodOptional<z.ZodString>;
|
|
195
256
|
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
@@ -222,6 +283,7 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
222
283
|
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
223
284
|
model: z.ZodOptional<z.ZodString>;
|
|
224
285
|
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
286
|
+
labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
225
287
|
vault: z.ZodOptional<z.ZodObject<{
|
|
226
288
|
storage: z.ZodOptional<z.ZodAny>;
|
|
227
289
|
scopeId: z.ZodOptional<z.ZodAny>;
|
|
@@ -233,17 +295,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
233
295
|
scopeId?: any;
|
|
234
296
|
}>>;
|
|
235
297
|
}, "strip", z.ZodTypeAny, {
|
|
298
|
+
model?: string | undefined;
|
|
299
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
236
300
|
mode?: "ner" | "classifier" | undefined;
|
|
237
301
|
reversible?: boolean | undefined;
|
|
238
|
-
model?: string | undefined;
|
|
239
302
|
vault?: {
|
|
240
303
|
storage?: any;
|
|
241
304
|
scopeId?: any;
|
|
242
305
|
} | undefined;
|
|
243
306
|
}, {
|
|
307
|
+
model?: string | undefined;
|
|
308
|
+
labelMappings?: Record<string, string | null> | undefined;
|
|
244
309
|
mode?: "ner" | "classifier" | undefined;
|
|
245
310
|
reversible?: boolean | undefined;
|
|
246
|
-
model?: string | undefined;
|
|
247
311
|
vault?: {
|
|
248
312
|
storage?: any;
|
|
249
313
|
scopeId?: any;
|
|
@@ -253,14 +317,17 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
253
317
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
254
318
|
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
255
319
|
serviceName: z.ZodOptional<z.ZodString>;
|
|
320
|
+
onDecision: z.ZodOptional<z.ZodAny>;
|
|
256
321
|
}, "strip", z.ZodTypeAny, {
|
|
257
322
|
enabled?: boolean | undefined;
|
|
258
323
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
259
324
|
serviceName?: string | undefined;
|
|
325
|
+
onDecision?: any;
|
|
260
326
|
}, {
|
|
261
327
|
enabled?: boolean | undefined;
|
|
262
328
|
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
263
329
|
serviceName?: string | undefined;
|
|
330
|
+
onDecision?: any;
|
|
264
331
|
}>>;
|
|
265
332
|
models: z.ZodOptional<z.ZodObject<{
|
|
266
333
|
extractor: z.ZodOptional<z.ZodString>;
|
|
@@ -271,6 +338,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
|
|
|
271
338
|
}>>;
|
|
272
339
|
}, z.ZodTypeAny, "passthrough">>, void>;
|
|
273
340
|
export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
|
|
274
|
-
|
|
341
|
+
type GuardHooks = ReturnType<typeof createGuardHooks>;
|
|
342
|
+
type NativeGuard = ReturnType<typeof guardMiddleware> & GuardHooks;
|
|
343
|
+
type LegacyGuard = ((req: any, ctxOrNext: any, maybeNext?: any) => Promise<any>) & GuardHooks;
|
|
344
|
+
export declare function guard(config: GuardConfig & {
|
|
345
|
+
tools: ToolGuardConfig;
|
|
346
|
+
}): NativeGuard;
|
|
347
|
+
export declare function guard(config?: GuardConfig & {
|
|
348
|
+
tools?: undefined;
|
|
349
|
+
}): LegacyGuard;
|
|
350
|
+
export declare function guard(config: GuardConfig): NativeGuard | LegacyGuard;
|
|
275
351
|
export declare const guardAction: typeof guard;
|
|
352
|
+
declare function createGuardHooks(config?: GuardConfig): {
|
|
353
|
+
model: (req: any, ctx: any, next: any) => Promise<any>;
|
|
354
|
+
tool: (req: any, ctx: any, next: any) => Promise<any>;
|
|
355
|
+
};
|
|
276
356
|
export {};
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { GuardToolError } from '../core/decision.js';
|
|
3
|
+
import { resolveGuardModels } from '../guard.config.js';
|
|
1
4
|
import { generateMiddleware, z } from 'genkit';
|
|
2
5
|
import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
|
|
3
6
|
import { detectPII } from '../pii/detector.js';
|
|
4
7
|
import { PiiTokenizer } from '../pii/tokenizer.js';
|
|
5
8
|
import { defaultPiiVaultStorage } from '../pii/storage.js';
|
|
6
9
|
const GUARD_CONTEXT_KEY = '__genkitGuard';
|
|
10
|
+
const toolActionSchema = z.enum(['allow', 'block', 'redact', 'approval-required']);
|
|
7
11
|
const guardConfigSchema = z.object({
|
|
12
|
+
policyVersion: z.string().optional(),
|
|
13
|
+
tools: z.object({
|
|
14
|
+
defaultAction: toolActionSchema.optional(),
|
|
15
|
+
rules: z.record(z.string(), toolActionSchema).optional(),
|
|
16
|
+
approve: z.any().optional(),
|
|
17
|
+
}).optional(),
|
|
8
18
|
intent: z.object({
|
|
9
19
|
mode: z.string().optional(),
|
|
10
20
|
allowedIntent: z.string().optional(),
|
|
@@ -17,6 +27,7 @@ const guardConfigSchema = z.object({
|
|
|
17
27
|
reversible: z.boolean().optional(),
|
|
18
28
|
model: z.string().optional(),
|
|
19
29
|
mode: z.enum(['ner', 'classifier']).optional(),
|
|
30
|
+
labelMappings: z.record(z.string(), z.string().regex(/^[A-Z_]+$/).nullable()).optional(),
|
|
20
31
|
vault: z.object({
|
|
21
32
|
storage: z.any().optional(),
|
|
22
33
|
scopeId: z.any().optional(),
|
|
@@ -26,6 +37,7 @@ const guardConfigSchema = z.object({
|
|
|
26
37
|
enabled: z.boolean().optional(),
|
|
27
38
|
level: z.enum(['debug', 'info', 'warn', 'error']).optional(),
|
|
28
39
|
serviceName: z.string().optional(),
|
|
40
|
+
onDecision: z.any().optional(),
|
|
29
41
|
}).optional(),
|
|
30
42
|
models: z.object({
|
|
31
43
|
extractor: z.string().optional(),
|
|
@@ -40,6 +52,10 @@ export const guardPlugin = guardMiddleware.plugin;
|
|
|
40
52
|
export function guard(config) {
|
|
41
53
|
const hooks = createGuardHooks(config);
|
|
42
54
|
const baseMiddleware = guardMiddleware(config);
|
|
55
|
+
// Genkit treats every function as legacy model-only middleware, ignoring tool hooks.
|
|
56
|
+
// New tool policies must use a native reference; legacy configurations stay callable.
|
|
57
|
+
if (config?.tools)
|
|
58
|
+
return Object.assign(baseMiddleware, hooks);
|
|
43
59
|
const fnRunner = async (req, ctxOrNext, maybeNext) => {
|
|
44
60
|
if (typeof maybeNext === 'function') {
|
|
45
61
|
return hooks.model(req, ctxOrNext, maybeNext);
|
|
@@ -62,8 +78,20 @@ export function guard(config) {
|
|
|
62
78
|
export const guardAction = guard;
|
|
63
79
|
function createGuardHooks(config) {
|
|
64
80
|
const logger = createLogger(config);
|
|
81
|
+
const models = resolveGuardModels(config);
|
|
82
|
+
const decide = async (start, fields) => {
|
|
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
|
+
};
|
|
65
92
|
return {
|
|
66
93
|
model: async (req, ctx, next) => {
|
|
94
|
+
const started = performance.now();
|
|
67
95
|
const input = getInputText(req);
|
|
68
96
|
logger('info', 'guard.model.start', 'Starting guard checks for model request');
|
|
69
97
|
const isInjection = await detectInjection(input);
|
|
@@ -71,17 +99,25 @@ function createGuardHooks(config) {
|
|
|
71
99
|
logger('warn', 'guard.intent.blocked', 'Prompt injection pattern detected', {
|
|
72
100
|
reason: 'pattern_match',
|
|
73
101
|
});
|
|
102
|
+
await decide(started, { guard: 'injection', action: 'block', reasonCode: 'INJECTION_PATTERN' });
|
|
74
103
|
return block('Prompt injection detected', {
|
|
75
104
|
reason: 'pattern_match',
|
|
76
105
|
});
|
|
77
106
|
}
|
|
107
|
+
await decide(started, { guard: 'injection', action: 'allow', reasonCode: 'INJECTION_CLEAR' });
|
|
108
|
+
const intentStarted = performance.now();
|
|
78
109
|
logger('info', 'guard.intent.analysis.start', 'Analyzing request intent');
|
|
79
|
-
const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7);
|
|
110
|
+
const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7, models.extractor);
|
|
80
111
|
logger('info', 'guard.intent.analysis.complete', 'Intent analysis completed', {
|
|
81
112
|
intent: intentResult.intent,
|
|
82
113
|
score: roundScore(intentResult.score),
|
|
83
114
|
allowed: intentResult.allowed,
|
|
84
115
|
});
|
|
116
|
+
await decide(intentStarted, {
|
|
117
|
+
guard: 'intent', action: intentResult.allowed ? 'allow' : 'block',
|
|
118
|
+
reasonCode: intentResult.allowed ? 'INTENT_ALLOWED' : 'INTENT_REJECTED',
|
|
119
|
+
confidence: intentResult.score,
|
|
120
|
+
});
|
|
85
121
|
if (!intentResult.allowed) {
|
|
86
122
|
logger('warn', 'guard.intent.blocked', 'Intent not allowed', {
|
|
87
123
|
intent: intentResult.intent,
|
|
@@ -92,6 +128,7 @@ function createGuardHooks(config) {
|
|
|
92
128
|
score: intentResult.score,
|
|
93
129
|
});
|
|
94
130
|
}
|
|
131
|
+
const piiStarted = performance.now();
|
|
95
132
|
const textForPii = collectModelRequestText(req);
|
|
96
133
|
const piiResponse = await scanPII(textForPii, config);
|
|
97
134
|
const piiMatches = piiResponse?.matches || [];
|
|
@@ -100,6 +137,8 @@ function createGuardHooks(config) {
|
|
|
100
137
|
await tokenizer.importTokens(textForPii);
|
|
101
138
|
await maskModelRequest(req, tokenizer, piiMatches);
|
|
102
139
|
pushTokenizer(ctx, tokenizer);
|
|
140
|
+
await decide(piiStarted, { guard: 'pii', action: piiMatches.length ? 'redact' : 'allow',
|
|
141
|
+
reasonCode: piiMatches.length ? 'PII_DETECTED' : 'PII_CLEAR' });
|
|
103
142
|
logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.model.pii.masked', 'PII scan completed for model request', {
|
|
104
143
|
piiDetected: piiMatches.length > 0,
|
|
105
144
|
piiMatchCount: piiMatches.length,
|
|
@@ -126,11 +165,44 @@ function createGuardHooks(config) {
|
|
|
126
165
|
return unmaskedResponse;
|
|
127
166
|
},
|
|
128
167
|
tool: async (req, ctx, next) => {
|
|
129
|
-
const
|
|
168
|
+
const started = performance.now();
|
|
130
169
|
const toolName = req?.toolRequest?.name;
|
|
170
|
+
const stop = async (action, reasonCode) => {
|
|
171
|
+
throw new GuardToolError(await decide(started, { guard: 'tool', action, reasonCode }));
|
|
172
|
+
};
|
|
173
|
+
const rules = config?.tools?.rules;
|
|
174
|
+
const action = rules && Object.hasOwn(rules, toolName)
|
|
175
|
+
? rules[toolName] : config?.tools?.defaultAction ?? 'allow';
|
|
176
|
+
if (!['allow', 'block', 'redact', 'approval-required'].includes(action)) {
|
|
177
|
+
return stop('block', 'TOOL_POLICY_ERROR');
|
|
178
|
+
}
|
|
179
|
+
if (action === 'block')
|
|
180
|
+
return stop('block', 'TOOL_BLOCKED');
|
|
181
|
+
if (action === 'approval-required' && !config?.tools?.approve) {
|
|
182
|
+
return stop('approval-required', 'TOOL_APPROVAL_REQUIRED');
|
|
183
|
+
}
|
|
184
|
+
const state = getGuardState(ctx);
|
|
185
|
+
// Genkit may provide a fresh middleware context for a tool turn. Create a recovery
|
|
186
|
+
// tokenizer that uses the configured vault so opaque tokens can be rehydrated safely.
|
|
187
|
+
if (state.tokenizers.length === 0) {
|
|
188
|
+
state.tokenizers.push(createTokenizer(config, req, ctx));
|
|
189
|
+
}
|
|
131
190
|
if (req?.toolRequest && 'input' in req.toolRequest) {
|
|
132
191
|
req.toolRequest.input = await unmaskObject(req.toolRequest.input, state.tokenizers);
|
|
133
192
|
}
|
|
193
|
+
if (action === 'approval-required') {
|
|
194
|
+
let approved;
|
|
195
|
+
try {
|
|
196
|
+
approved = await config.tools.approve({
|
|
197
|
+
toolName, input: structuredClone(req?.toolRequest?.input), context: ctx?.context,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return stop('block', 'TOOL_POLICY_ERROR');
|
|
202
|
+
}
|
|
203
|
+
if (approved !== true)
|
|
204
|
+
return stop('block', 'TOOL_APPROVAL_DENIED');
|
|
205
|
+
}
|
|
134
206
|
const toolInputText = collectStrings(req?.toolRequest?.input).join('\n');
|
|
135
207
|
const piiResponse = await scanPII(toolInputText, config);
|
|
136
208
|
const piiMatches = piiResponse?.matches || [];
|
|
@@ -147,6 +219,18 @@ function createGuardHooks(config) {
|
|
|
147
219
|
piiMatchCount: piiMatches.length,
|
|
148
220
|
piiTypes,
|
|
149
221
|
});
|
|
222
|
+
if (action === 'redact' && req?.toolRequest) {
|
|
223
|
+
req.toolRequest.input = await transformStrings(req.toolRequest.input, (value) => {
|
|
224
|
+
// Irreversible redaction: do not send recoverable vault tokens to this tool.
|
|
225
|
+
for (const match of [...piiMatches].sort((a, b) => b.value.length - a.value.length)) {
|
|
226
|
+
if (match.value)
|
|
227
|
+
value = value.split(match.value).join('[REDACTED]');
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
await decide(started, { guard: 'tool', action: action === 'redact' ? 'redact' : 'allow',
|
|
233
|
+
reasonCode: action === 'redact' ? 'TOOL_REDACTED' : action === 'approval-required' ? 'TOOL_APPROVED' : 'TOOL_ALLOWED' });
|
|
150
234
|
const res = await next(req, ctx);
|
|
151
235
|
const toolResponseText = collectStrings(res).join('\n');
|
|
152
236
|
if (toolResponseText) {
|
|
@@ -199,6 +283,9 @@ async function unmaskObject(obj, tokenizers) {
|
|
|
199
283
|
return transformStrings(obj, async (value) => {
|
|
200
284
|
let result = value;
|
|
201
285
|
for (const tokenizer of tokenizers) {
|
|
286
|
+
// A token may have been produced by another model/tool turn with a different Genkit
|
|
287
|
+
// context or vault scope. Import only opaque tokens actually present in this value.
|
|
288
|
+
await tokenizer.importTokens(result);
|
|
202
289
|
result = await tokenizer.unmask(result);
|
|
203
290
|
}
|
|
204
291
|
return result;
|
|
@@ -241,9 +328,11 @@ async function scanPII(text, config) {
|
|
|
241
328
|
classifier: undefined,
|
|
242
329
|
};
|
|
243
330
|
}
|
|
331
|
+
const models = resolveGuardModels(config);
|
|
244
332
|
return detectPII(text, {
|
|
245
|
-
model:
|
|
246
|
-
mode:
|
|
333
|
+
model: models.pii,
|
|
334
|
+
mode: models.mode,
|
|
335
|
+
labelMappings: config?.pii?.labelMappings,
|
|
247
336
|
});
|
|
248
337
|
}
|
|
249
338
|
function getGuardState(ctx = {}) {
|
package/dist/pii/detector.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
import type { PiiLabelMappings } from '../guard.config.js';
|
|
2
|
+
export type PiiMatch = {
|
|
3
|
+
type: string;
|
|
4
|
+
value: string;
|
|
5
|
+
};
|
|
6
|
+
export type PrivacyFilterSpan = {
|
|
7
|
+
entity_group?: string;
|
|
8
|
+
entity?: string;
|
|
9
|
+
word?: string;
|
|
10
|
+
start?: number;
|
|
11
|
+
end?: number;
|
|
12
|
+
score?: number;
|
|
13
|
+
};
|
|
1
14
|
export declare function detectPII(text: string, opts?: {
|
|
2
15
|
model?: string;
|
|
3
16
|
mode?: 'ner' | 'classifier';
|
|
17
|
+
labelMappings?: PiiLabelMappings;
|
|
4
18
|
}): Promise<{
|
|
5
|
-
matches:
|
|
6
|
-
type: string;
|
|
7
|
-
value: string;
|
|
8
|
-
}[];
|
|
19
|
+
matches: PiiMatch[];
|
|
9
20
|
classifier: any;
|
|
10
21
|
}>;
|
|
22
|
+
export declare function privacyFilterOutputToMatches(text: string, output: unknown, labelMappings?: PiiLabelMappings): PiiMatch[];
|
package/dist/pii/detector.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { ModelSingleton } from '../util/singleton.js';
|
|
2
|
+
const PRIVACY_FILTER_TYPE_MAP = {
|
|
3
|
+
account_number: 'ACCOUNT_NUMBER',
|
|
4
|
+
private_address: 'ADDRESS',
|
|
5
|
+
private_email: 'EMAIL',
|
|
6
|
+
private_person: 'NAME',
|
|
7
|
+
private_phone: 'PHONE',
|
|
8
|
+
private_url: 'URL',
|
|
9
|
+
private_date: 'DATE',
|
|
10
|
+
secret: 'SECRET',
|
|
11
|
+
};
|
|
2
12
|
const REGEX_RULES = [
|
|
3
13
|
// EMAIL (keep your existing one)
|
|
4
14
|
{ type: 'EMAIL', pattern: /\b[\w\.-]+@[\w\.-]+\.\w{2,}\b/gi },
|
|
@@ -30,18 +40,77 @@ export async function detectPII(text, opts) {
|
|
|
30
40
|
const ner = await ModelSingleton.getNER(model);
|
|
31
41
|
const entities = await ner(text);
|
|
32
42
|
for (const e of entities) {
|
|
33
|
-
|
|
43
|
+
const mappedType = mappedLabel(e.entity_group ?? e.entity, opts?.labelMappings);
|
|
44
|
+
if (mappedType !== undefined) {
|
|
45
|
+
if (mappedType)
|
|
46
|
+
results.push(...privacyFilterOutputToMatches(text, [e], opts?.labelMappings));
|
|
47
|
+
}
|
|
48
|
+
else if (e.entity && e.entity.includes('PER')) {
|
|
34
49
|
results.push({ type: 'NAME', value: (e.word || '').replace(/##/g, '') });
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
else {
|
|
39
|
-
//
|
|
54
|
+
// Privacy Filter is a token-classification model. Aggregation produces complete spans
|
|
55
|
+
// rather than individual BIOES-labelled tokens.
|
|
40
56
|
const cls = await ModelSingleton.getPIIClassifier(model);
|
|
41
|
-
classifierOutput = await cls(text);
|
|
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)) {
|
|
60
|
+
results.push(match);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
42
63
|
}
|
|
43
64
|
return {
|
|
44
65
|
matches: results,
|
|
45
66
|
classifier: classifierOutput
|
|
46
67
|
};
|
|
47
68
|
}
|
|
69
|
+
export function privacyFilterOutputToMatches(text, output, labelMappings) {
|
|
70
|
+
if (!Array.isArray(output))
|
|
71
|
+
return [];
|
|
72
|
+
const matches = [];
|
|
73
|
+
for (const candidate of output) {
|
|
74
|
+
if (!candidate || typeof candidate !== 'object')
|
|
75
|
+
continue;
|
|
76
|
+
const span = candidate;
|
|
77
|
+
const rawLabel = span.entity_group ?? span.entity;
|
|
78
|
+
if (typeof rawLabel !== 'string')
|
|
79
|
+
continue;
|
|
80
|
+
const label = rawLabel.replace(/^[BIES]-/, '').toLowerCase();
|
|
81
|
+
const customType = mappedLabel(rawLabel, labelMappings);
|
|
82
|
+
const type = customType !== undefined ? customType : (Object.hasOwn(PRIVACY_FILTER_TYPE_MAP, label) ? PRIVACY_FILTER_TYPE_MAP[label] : undefined);
|
|
83
|
+
if (!type)
|
|
84
|
+
continue;
|
|
85
|
+
let value;
|
|
86
|
+
if (Number.isInteger(span.start) &&
|
|
87
|
+
Number.isInteger(span.end) &&
|
|
88
|
+
span.start >= 0 &&
|
|
89
|
+
span.end > span.start &&
|
|
90
|
+
span.end <= text.length) {
|
|
91
|
+
value = text.slice(span.start, span.end);
|
|
92
|
+
}
|
|
93
|
+
else if (typeof span.word === 'string') {
|
|
94
|
+
value = span.word.trim();
|
|
95
|
+
}
|
|
96
|
+
if (!value || !text.includes(value))
|
|
97
|
+
continue;
|
|
98
|
+
if (!matches.some((existing) => existing.value === value)) {
|
|
99
|
+
matches.push({ type, value });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return matches;
|
|
103
|
+
}
|
|
104
|
+
function mappedLabel(label, mappings) {
|
|
105
|
+
if (typeof label !== 'string' || !mappings)
|
|
106
|
+
return undefined;
|
|
107
|
+
const normalize = (value) => value.replace(/^[BIES]-/i, '').toLowerCase();
|
|
108
|
+
const entry = Object.entries(mappings).find(([key]) => normalize(key) === normalize(label));
|
|
109
|
+
if (!entry)
|
|
110
|
+
return undefined;
|
|
111
|
+
const type = entry[1];
|
|
112
|
+
if (type !== null && !/^[A-Z_]+$/.test(type)) {
|
|
113
|
+
throw new Error('PII label mapping types must contain only uppercase letters and underscores');
|
|
114
|
+
}
|
|
115
|
+
return type;
|
|
116
|
+
}
|
package/dist/util/singleton.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
export declare const PRIVACY_FILTER_PIPELINE_TASK: "token-classification";
|
|
1
2
|
export declare class ModelSingleton {
|
|
2
3
|
private static extractors;
|
|
3
4
|
private static nerClassifiers;
|
|
4
|
-
private static
|
|
5
|
+
private static privacyFilters;
|
|
5
6
|
static init(): void;
|
|
6
7
|
static getExtractor(modelName?: string): Promise<any>;
|
|
7
8
|
static getNER(modelName?: string): Promise<any>;
|
package/dist/util/singleton.js
CHANGED
|
@@ -3,12 +3,13 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const PRIVACY_FILTER_PIPELINE_TASK = 'token-classification';
|
|
6
7
|
env.allowRemoteModels = false;
|
|
7
8
|
env.localModelPath = path.join(__dirname, '../../models');
|
|
8
9
|
export class ModelSingleton {
|
|
9
10
|
static extractors = new Map();
|
|
10
11
|
static nerClassifiers = new Map();
|
|
11
|
-
static
|
|
12
|
+
static privacyFilters = new Map();
|
|
12
13
|
static init() {
|
|
13
14
|
// Always resolve model path relative to the client app, not the library
|
|
14
15
|
const projectRoot = process.cwd();
|
|
@@ -55,12 +56,14 @@ export class ModelSingleton {
|
|
|
55
56
|
return this.nerClassifiers.get(modelName);
|
|
56
57
|
}
|
|
57
58
|
static async getPIIClassifier(modelName = 'openai/privacy-filter') {
|
|
58
|
-
if (!this.
|
|
59
|
+
if (!this.privacyFilters.has(modelName)) {
|
|
59
60
|
this.init();
|
|
60
|
-
const inst = await pipeline(
|
|
61
|
-
|
|
61
|
+
const inst = await pipeline(PRIVACY_FILTER_PIPELINE_TASK, modelName, {
|
|
62
|
+
dtype: 'q4',
|
|
63
|
+
});
|
|
64
|
+
this.privacyFilters.set(modelName, inst);
|
|
62
65
|
}
|
|
63
|
-
return this.
|
|
66
|
+
return this.privacyFilters.get(modelName);
|
|
64
67
|
}
|
|
65
68
|
static async preload(models) {
|
|
66
69
|
const tasks = [];
|
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.14",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
@@ -34,13 +34,18 @@
|
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
36
|
"prepare-models": "node scripts/download-model.js",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
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
40
|
"test:types": "tsc --noEmit --ignoreConfig --module NodeNext --moduleResolution NodeNext --target ESNext --strict --skipLibCheck scripts/test-types.ts",
|
|
41
|
+
"test:privacy-filter": "npm run build && node scripts/test-privacy-filter.js",
|
|
40
42
|
"test:storage": "npm run build && node scripts/test-storage.js",
|
|
41
43
|
"test:concurrency": "npm run build && node scripts/test-concurrent-vault.js",
|
|
42
44
|
"test:redis": "npm run build && node scripts/test-redis-vault.js",
|
|
43
|
-
"prepublishOnly": "npm run build"
|
|
45
|
+
"prepublishOnly": "npm run build",
|
|
46
|
+
"test:config": "npm run build && node scripts/test-config.js",
|
|
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"
|
|
44
49
|
},
|
|
45
50
|
"dependencies": {
|
|
46
51
|
"@huggingface/transformers": "^4.2.0",
|