@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.
@@ -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
- export declare function guard(config?: GuardConfig): (req: any, ctxOrNext: any, maybeNext?: any) => Promise<any>;
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 state = getGuardState(ctx);
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: config?.pii?.model,
246
- mode: config?.pii?.mode,
333
+ model: models.pii,
334
+ mode: models.mode,
335
+ labelMappings: config?.pii?.labelMappings,
247
336
  });
248
337
  }
249
338
  function getGuardState(ctx = {}) {
@@ -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[];
@@ -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
- if (e.entity && e.entity.includes('PER')) {
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
- // classifier mode: we call the classifier and return its output alongside regex matches.
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
+ }
@@ -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 textClassifiers;
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>;
@@ -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 textClassifiers = new Map();
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.textClassifiers.has(modelName)) {
59
+ if (!this.privacyFilters.has(modelName)) {
59
60
  this.init();
60
- const inst = await pipeline('text-classification', modelName);
61
- this.textClassifiers.set(modelName, inst);
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.textClassifiers.get(modelName);
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.12",
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
- "build": "tsc",
38
- "test": "npm run test:types && npm run test:storage && npm run test:concurrency",
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",