@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 CHANGED
@@ -17,15 +17,36 @@ Agents can access customer data and trigger real workflows. Genkit Guard gives y
17
17
 
18
18
  Detection is based on patterns and model predictions. It can miss attacks or PII and can reject valid requests; evaluate it on your application's inputs and keep authorization in your application.
19
19
 
20
- ## Quick start
20
+ ## Full setup guide
21
21
 
22
- Install the package in your Genkit application:
22
+ The following setup targets **v0.1.0** and can be used after that version is published. The package declares Genkit **1.39.0** as its peer dependency.
23
+
24
+ ### 1. Create the application and install dependencies
25
+
26
+ ```bash
27
+ mkdir my-genkit-app
28
+ cd my-genkit-app
29
+ npm init -y
30
+ npm pkg set type=module
31
+ npm install genkit@1.39.0 @genkit-ai/google-genai@1.39.0 @intflows/genkit-guard@0.1.0
32
+ npm install -D typescript tsx @types/node
33
+ mkdir src
34
+ ```
35
+
36
+ For an existing Genkit application, install the guard package and use your existing provider configuration.
37
+
38
+ ### 2. Download the models
39
+
40
+ Download MiniLM for intent analysis and OpenAI's `privacy-filter` for PII detection. Run this command once from your application's root directory:
23
41
 
24
42
  ```bash
25
- npm install @intflows/genkit-guard
43
+ # Download MiniLM + OpenAI/privacy-filter
44
+ node node_modules/@intflows/genkit-guard/scripts/download-model.js
26
45
  ```
27
46
 
28
- The examples below target **v0.0.14**. The package declares Genkit **1.39.0** as its peer dependency.
47
+ Models are cached locally in `./models` and reused across runs. This script downloads those two models; it does not read `guard.config.ts`. The configuration below selects `pii.mode: "classifier"` to use `privacy-filter`. Download size and memory use depend on the models selected.
48
+
49
+ ### 3. Configure the guard
29
50
 
30
51
  Create `src/guard.config.ts`:
31
52
 
@@ -53,17 +74,32 @@ export default defineGuardConfig({
53
74
  });
54
75
  ```
55
76
 
56
- Use the same configuration at startup and in your model call:
77
+ A separate configuration file is optional in general, but required by the import in this example. Pass the same configuration to startup and runtime; there is no automatic file discovery. See [shared model configuration](https://github.com/IntFlows/genkit-guard/wiki/8.-Shared-Model-Configuration) for alternatives.
78
+
79
+ The tool rules apply to tools you separately register and supply to Genkit; they do not create tools. This configuration permits `searchDocs` and blocks other tool names.
80
+
81
+ ### 4. Create the application entry point
82
+
83
+ Create `src/index.ts`:
57
84
 
58
85
  ```ts
86
+ import { genkit } from "genkit";
87
+ import { googleAI } from "@genkit-ai/google-genai";
59
88
  import { guard, initGuard } from "@intflows/genkit-guard";
60
89
  import guardConfig from "./guard.config.js";
61
90
 
62
- // ai is your configured Genkit instance.
91
+ const modelName = process.env.GEMINI_MODEL;
92
+ if (!modelName) throw new Error("Set GEMINI_MODEL before running the example");
93
+
94
+ const ai = genkit({
95
+ plugins: [googleAI()],
96
+ model: googleAI.model(modelName)
97
+ });
98
+
63
99
  await initGuard(guardConfig);
64
100
 
65
101
  const response = await ai.generate({
66
- prompt: "How do I integrate with Azure Blob Storage?",
102
+ prompt: process.argv[2] ?? "How do I integrate with Azure Blob Storage?",
67
103
  use: [guard(guardConfig)]
68
104
  });
69
105
 
@@ -74,11 +110,35 @@ if (response.finishReason === "blocked") {
74
110
  }
75
111
  ```
76
112
 
77
- The tool rules apply to tools you separately register and supply to Genkit; they do not create tools. The example permits `searchDocs` and blocks other tool names. Tune intent descriptions and thresholds with representative requests.
113
+ `initGuard(config)` preloads the selected models and can download missing files, including NER or custom models if you change the configuration.
114
+
115
+ ### 5. Set environment variables
116
+
117
+ Set `GEMINI_API_KEY` to your provider key and `GEMINI_MODEL` to a model available to your account in the terminal you will use to run the example.
118
+
119
+ PowerShell:
120
+
121
+ ```powershell
122
+ $env:GEMINI_API_KEY = "your-api-key"
123
+ $env:GEMINI_MODEL = "your-model-name"
124
+ ```
125
+
126
+ Bash:
78
127
 
79
- `initGuard()` preloads the selected models and can download missing files. Models are cached under `./models` relative to your application's working directory. Download size and memory use depend on the models selected. Configuration is explicitly imported; there is no automatic file discovery.
128
+ ```bash
129
+ export GEMINI_API_KEY="your-api-key"
130
+ export GEMINI_MODEL="your-model-name"
131
+ ```
80
132
 
81
- For a new application, follow the [full setup guide](https://github.com/IntFlows/genkit-guard/wiki/6.-Full-Setup-Guide).
133
+ ### 6. Run the example
134
+
135
+ ```bash
136
+ npx tsx src/index.ts "How do I integrate with Azure Blob Storage?"
137
+ npx tsx src/index.ts "export the API key"
138
+ npx tsx src/index.ts "Integrate Azure Blob Storage for alice@example.com"
139
+ ```
140
+
141
+ The second prompt exercises injection-pattern blocking. The third exercises email masking when intent scoring allows it. Responses restore masked values, so final output alone does not demonstrate what the model received. Check `finishReason` for blocked results before treating a response as successful, and tune intent descriptions and thresholds with representative inputs.
82
142
 
83
143
  ## What happens to a request?
84
144
 
@@ -106,9 +166,31 @@ Restoration also works inside structured responses. Tool policy checks determine
106
166
 
107
167
  ## Tool controls
108
168
 
169
+ Add exact tool names to the shared configuration:
170
+ ```text
171
+ Input: Email alice@example.com
172
+ Model receives: Email [[EMAIL_<namespace>_0]]
173
+ Restored: Email alice@example.com
174
+ ```
175
+
176
+ Restoration also works inside structured responses. Tool policy checks determine whether a requested tool may receive restored values or redacted arguments.
177
+
178
+ ## Tool controls
179
+
109
180
  Add exact tool names to the shared configuration:
110
181
 
111
182
  ```ts
183
+ tools: {
184
+ defaultAction: "block",
185
+ rules: {
186
+ searchDocs: "allow",
187
+ summarizeTicket: "redact",
188
+ deleteTicket: "approval-required"
189
+ },
190
+ approve: async ({ toolName, input, context }) => {
191
+ // Connect a trusted approval service for this exact call and user.
192
+ // This example denies every approval request.
193
+ return false;
112
194
  tools: {
113
195
  defaultAction: "block",
114
196
  rules: {
@@ -156,8 +238,43 @@ Use `models.extractor` and `pii.model` to select compatible models, and `pii.lab
156
238
 
157
239
  ## PII Vault Isolation and External Storage
158
240
 
241
+ Masked values are kept in a vault so they can be restored later. By default, the middleware uses process-local in-memory storage with generated vault scopes. Each tokenizer also generates an opaque namespace for its placeholders:
242
+ | Policy | Behavior |
243
+ | --- | --- |
244
+ | `allow` | Restore tokens, scan input, then execute |
245
+ | `block` | Stop before execution |
246
+ | `redact` | Replace detected PII in nested string arguments with `[REDACTED]`, then execute |
247
+ | `approval-required` | Execute only when the application's callback returns literal `true` |
248
+
249
+ Blocked, pending, or denied calls throw `GuardToolError`. Approval UI and durable approval storage belong to the application. Redaction may invalidate a tool's input schema, such as an email field; choose a policy appropriate to the tool.
250
+
251
+ Without tool policies, the default remains allow. With `tools` configured, `guard()` returns a native Genkit middleware reference. Existing configurations without `tools` retain the legacy callable form. Use `guardMiddleware(config)` for native tool hooks without explicit policies.
252
+
253
+ See [tool controls and logging](https://github.com/IntFlows/genkit-guard/wiki/9.-Tool-Controls-and-Logging) for the complete behavior.
254
+
255
+ ## Models and PII storage
256
+
257
+ | Setting | Default |
258
+ | --- | --- |
259
+ | Intent model | `Xenova/all-MiniLM-L6-v2` |
260
+ | PII mode | `ner` |
261
+ | NER model | `Xenova/bert-base-NER` |
262
+ | Classifier model | `openai/privacy-filter` when classifier mode is selected |
263
+ | PII vault | In-memory storage |
264
+
265
+ The quick start explicitly selects classifier mode. Regex detection also runs for email, Australian phone and identifier patterns, and credit-card-like numbers.
266
+
267
+ Use `models.extractor` and `pii.model` to select compatible models, and `pii.labelMappings` to map fine-tuned labels to masking types. Classifier mode currently loads `q4` weights. Redis and custom vault adapters support external storage.
268
+
269
+ - [Shared model configuration](https://github.com/IntFlows/genkit-guard/wiki/8.-Shared-Model-Configuration)
270
+ - [PII labels, masking and vaults](https://github.com/IntFlows/genkit-guard/wiki/5.-PII-Guard)
271
+
272
+ ## PII Vault Isolation and External Storage
273
+
159
274
  Masked values are kept in a vault so they can be restored later. By default, the middleware uses process-local in-memory storage with generated vault scopes. Each tokenizer also generates an opaque namespace for its placeholders:
160
275
 
276
+ ```text
277
+ alice@example.com -> [[EMAIL_<namespace>_0]]
161
278
  ```text
162
279
  alice@example.com -> [[EMAIL_<namespace>_0]]
163
280
  ```
@@ -172,20 +289,38 @@ Use Redis when vault entries need to survive application restarts or be availabl
172
289
  npm install redis
173
290
  ```
174
291
 
292
+ Extend your shared configuration during application startup:
293
+ Namespaces prevent concurrent calls from creating identical placeholder names. Use `pii.vault.scopeId` to group vault entries by request, session, or another application scope. The scope ID is not exposed in the placeholder.
294
+
295
+ ### Redis storage
296
+
297
+ Use Redis when vault entries need to survive application restarts or be available to multiple workers. Install the client separately:
298
+
299
+ ```bash
300
+ npm install redis
301
+ ```
302
+
175
303
  Extend your shared configuration during application startup:
176
304
 
177
305
  ```ts
178
306
  import { createClient } from "redis";
179
307
  import { guard, initGuard, createRedisPiiVaultStorage } from "@intflows/genkit-guard";
180
308
  import guardConfig from "./guard.config.js";
309
+ import { guard, initGuard, createRedisPiiVaultStorage } from "@intflows/genkit-guard";
310
+ import guardConfig from "./guard.config.js";
181
311
 
312
+ const redis = createClient({ url: process.env.REDIS_URL ?? "redis://localhost:6379" });
313
+ redis.on("error", () => console.error("PII vault Redis connection error"));
182
314
  const redis = createClient({ url: process.env.REDIS_URL ?? "redis://localhost:6379" });
183
315
  redis.on("error", () => console.error("PII vault Redis connection error"));
184
316
  await redis.connect();
185
317
 
318
+ const config = {
319
+ ...guardConfig,
186
320
  const config = {
187
321
  ...guardConfig,
188
322
  pii: {
323
+ ...guardConfig.pii,
189
324
  ...guardConfig.pii,
190
325
  vault: {
191
326
  storage: createRedisPiiVaultStorage(redis, {
@@ -219,21 +354,57 @@ See the [PII vault documentation](https://github.com/IntFlows/genkit-guard/wiki/
219
354
 
220
355
  ## Decision logging
221
356
 
222
- Console logging emits structured JSON. Add an audit callback to your configuration to receive `GuardDecision` events:
357
+ Console logging emits structured JSON. In v0.1.0, attach a persistent store to your shared configuration:
223
358
 
224
359
  ```ts
225
- policyVersion: "support-v1",
226
- logging: {
227
- enabled: true,
228
- level: "info",
229
- onDecision: async decision => {
230
- // Forward the content-free event to your audit sink.
231
- console.log(JSON.stringify(decision));
360
+ import { defineGuardConfig, createJsonlDecisionStore } from "@intflows/genkit-guard";
361
+
362
+ const decisionStore = createJsonlDecisionStore("./logs/guard-decisions.jsonl");
363
+ const config = defineGuardConfig({
364
+ // Include your intent, PII and tool policies here.
365
+ policyVersion: "support-v2",
366
+ logging: {
367
+ enabled: false, // Disable console output; persistence still runs.
368
+ store: decisionStore
369
+ }
370
+ });
371
+ ```
372
+
373
+ Pass `config` to `initGuard()` and `guard()`. Each append is validated, serialized and flushed before it resolves. Use `await decisionStore.read()` to load the records, including after restarting the application. Unknown fields are stripped before writing.
374
+
375
+ Events include schema version, decision ID, timestamp, guard, policy version, action, reason code, latency, and intent similarity where applicable. They exclude raw prompts, arguments, classifier output and error messages. Use non-sensitive policy identifiers.
376
+
377
+ `logging.onDecision` remains available. Store writes happen first, then the callback; both are awaited regardless of console settings. Failure stops the current operation. There are no automatic delivery retries, and a successful write is not rolled back if the callback later fails. Decisions describe checks, not confirmation of downstream execution.
378
+
379
+ The JSONL helper supports concurrent callers in one process, including separate instances using the same resolved path. Use separate files per worker or a shared backend through `createGuardDecisionStore({ append })` for multiple processes. File rotation, retention, encryption and crash recovery are application responsibilities; incomplete log tails cause reads and further appends to fail rather than silently discarding records. `read()` loads the full file into memory.
380
+
381
+ ## Fallback guard models
382
+
383
+ Fallbacks are optional and apply to local intent and PII models, not your application's generative model:
384
+
385
+ ```ts
386
+ models: {
387
+ extractor: "Xenova/all-MiniLM-L6-v2",
388
+ extractorFallback: "your-org/compatible-embedding-model"
389
+ },
390
+ pii: {
391
+ mode: "classifier",
392
+ model: "openai/privacy-filter",
393
+ fallback: {
394
+ model: "Xenova/bert-base-NER",
395
+ mode: "ner",
396
+ labelMappings: { PER: "NAME" }
232
397
  }
233
398
  }
234
399
  ```
235
400
 
236
- Events include schema version, decision ID, timestamp, guard, policy version, action, reason code, latency, and intent similarity where applicable. The callback runs independently of console settings, is awaited, and stops execution if it fails. It does not provide persistent storage by itself.
401
+ Replace the custom embedding identifier with a model you have prepared. A fallback runs once when the primary model fails to load or execute. An intent rejection or empty PII result does not trigger fallback. The primary is tried again on the next operation; there is no timeout, circuit breaker or automatic switch for future requests.
402
+
403
+ PII fallback mode defaults to the primary mode. Its label mappings are independent of the primary mappings. Both models must support the relevant Transformers.js task; classifier mode requires compatible `q4` weights. NER and privacy-filter have different detection coverage, so evaluate the fallback on your own inputs before enabling it.
404
+
405
+ Successful recovery emits `MODEL_FALLBACK_USED`; normal guard checks still determine whether the request proceeds. If both models fail, `GuardModelError` with code `MODEL_UNAVAILABLE` stops the operation. There is no regex-only bypass. The same policy applies during `initGuard()`, model-request checks and tool PII scans. A failure after a tool has executed cannot undo that execution.
406
+
407
+ Without fallback configuration, existing model selection and error propagation remain unchanged. See [persistent decisions and fallback models](https://github.com/IntFlows/genkit-guard/wiki/10.-Decision-Storage-and-Model-Fallback) for the full contract.
237
408
 
238
409
  ## Try it and explore the examples
239
410
 
@@ -252,13 +423,16 @@ npx tsx src/tool-controls.ts
252
423
  ## Roadmap
253
424
 
254
425
  - **v0.0.14:** shared model configuration, custom PII labels, tool policies and versioned decision events.
255
- - **v0.1.0 planned:** persistent decision logging and explicit fallback-model behavior.
426
+ - **v0.1.0 (in development):** persistent decision logging and explicit fallback-model behavior.
256
427
  - **Later:** SQLite vault, memory compaction integration, compatibility hardening and stable v1.
257
428
 
258
429
  ## Contributing
259
430
 
431
+ Issues, pull requests, model evaluations and security reviews are welcome. Run `npm test` for the deterministic suite; real Redis integration is tested separately with `npm run test:redis`.
260
432
  Issues, pull requests, model evaluations and security reviews are welcome. Run `npm test` for the deterministic suite; real Redis integration is tested separately with `npm run test:redis`.
261
433
 
434
+ ## License
262
435
  ## License
263
436
 
264
437
  [Apache-2.0](./LICENSE)
438
+ [Apache-2.0](./LICENSE)
@@ -0,0 +1,5 @@
1
+ import type { GuardConfig } from '../middleware/middleware.js';
2
+ import type { GuardDecision } from './decision.js';
3
+ export declare function publishDecision(config: GuardConfig | undefined, start: number, fields: Pick<GuardDecision, 'guard' | 'action' | 'reasonCode'> & {
4
+ confidence?: number;
5
+ }): Promise<GuardDecision>;
@@ -0,0 +1,23 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export async function publishDecision(config, start, fields) {
3
+ const decision = Object.freeze({
4
+ schemaVersion: '1', decisionId: randomUUID(), timestamp: new Date().toISOString(),
5
+ policyVersion: config?.policyVersion ?? 'unversioned',
6
+ latencyMs: Math.max(0, performance.now() - start), ...fields,
7
+ });
8
+ const warning = decision.action === 'block' || decision.action === 'approval-required';
9
+ const level = config?.logging?.level ?? 'info';
10
+ if ((config?.logging?.enabled ?? true) && level !== 'error' && (level !== 'warn' || warning)) {
11
+ const record = {
12
+ timestamp: decision.timestamp, severityText: warning ? 'WARN' : 'INFO',
13
+ severityNumber: warning ? 13 : 9, body: 'Guard policy decision',
14
+ resource: { attributes: { 'service.name': config?.logging?.serviceName ?? '@intflows/genkit-guard' } },
15
+ attributes: { 'event.name': 'guard.decision', 'code.namespace': 'genkit-guard', decision },
16
+ };
17
+ (warning ? console.warn : console.log)(JSON.stringify(record));
18
+ }
19
+ // Persist before invoking the callback. Neither failure may trigger a model fallback.
20
+ await config?.logging?.store?.append(decision);
21
+ await config?.logging?.onDecision?.(decision);
22
+ return decision;
23
+ }
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import type { GuardDecision } from './decision.js';
3
+ export interface GuardDecisionStore {
4
+ append(decision: GuardDecision): void | Promise<void>;
5
+ }
6
+ /** Validates the v1 contract and strips unknown fields before persistence. */
7
+ export declare const guardDecisionSchema: z.ZodObject<{
8
+ schemaVersion: z.ZodLiteral<"1">;
9
+ decisionId: z.ZodString;
10
+ timestamp: z.ZodString;
11
+ guard: z.ZodEnum<{
12
+ injection: "injection";
13
+ intent: "intent";
14
+ pii: "pii";
15
+ tool: "tool";
16
+ }>;
17
+ policyVersion: z.ZodString;
18
+ action: z.ZodEnum<{
19
+ allow: "allow";
20
+ block: "block";
21
+ redact: "redact";
22
+ "approval-required": "approval-required";
23
+ }>;
24
+ reasonCode: z.ZodEnum<{
25
+ INJECTION_PATTERN: "INJECTION_PATTERN";
26
+ INJECTION_CLEAR: "INJECTION_CLEAR";
27
+ INTENT_ALLOWED: "INTENT_ALLOWED";
28
+ INTENT_REJECTED: "INTENT_REJECTED";
29
+ PII_DETECTED: "PII_DETECTED";
30
+ PII_CLEAR: "PII_CLEAR";
31
+ TOOL_ALLOWED: "TOOL_ALLOWED";
32
+ TOOL_BLOCKED: "TOOL_BLOCKED";
33
+ TOOL_REDACTED: "TOOL_REDACTED";
34
+ TOOL_APPROVAL_REQUIRED: "TOOL_APPROVAL_REQUIRED";
35
+ TOOL_APPROVED: "TOOL_APPROVED";
36
+ TOOL_APPROVAL_DENIED: "TOOL_APPROVAL_DENIED";
37
+ TOOL_POLICY_ERROR: "TOOL_POLICY_ERROR";
38
+ MODEL_FALLBACK_USED: "MODEL_FALLBACK_USED";
39
+ MODEL_UNAVAILABLE: "MODEL_UNAVAILABLE";
40
+ }>;
41
+ latencyMs: z.ZodNumber;
42
+ confidence: z.ZodOptional<z.ZodNumber>;
43
+ }, z.core.$strip>;
44
+ export declare function createGuardDecisionStore(adapter: GuardDecisionStore): GuardDecisionStore;
45
+ export interface JsonlDecisionStore extends GuardDecisionStore {
46
+ /** Read the complete file. Missing files return []; malformed records reject. */
47
+ read(): Promise<GuardDecision[]>;
48
+ }
49
+ /** Single-process JSONL writer. Each append is flushed before it resolves. */
50
+ export declare function createJsonlDecisionStore(filePath: string): JsonlDecisionStore;
@@ -0,0 +1,84 @@
1
+ import { open, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { z } from 'zod';
4
+ /** Validates the v1 contract and strips unknown fields before persistence. */
5
+ export const guardDecisionSchema = z.object({
6
+ schemaVersion: z.literal('1'),
7
+ decisionId: z.string().min(1), timestamp: z.string().datetime(),
8
+ guard: z.enum(['injection', 'intent', 'pii', 'tool']),
9
+ policyVersion: z.string(),
10
+ action: z.enum(['allow', 'block', 'redact', 'approval-required']),
11
+ reasonCode: z.enum([
12
+ 'INJECTION_PATTERN', 'INJECTION_CLEAR', 'INTENT_ALLOWED', 'INTENT_REJECTED',
13
+ 'PII_DETECTED', 'PII_CLEAR', 'TOOL_ALLOWED', 'TOOL_BLOCKED', 'TOOL_REDACTED',
14
+ 'TOOL_APPROVAL_REQUIRED', 'TOOL_APPROVED', 'TOOL_APPROVAL_DENIED', 'TOOL_POLICY_ERROR',
15
+ 'MODEL_FALLBACK_USED', 'MODEL_UNAVAILABLE',
16
+ ]),
17
+ latencyMs: z.number().finite().nonnegative(), confidence: z.number().finite().optional(),
18
+ });
19
+ export function createGuardDecisionStore(adapter) {
20
+ return { append: decision => adapter.append(guardDecisionSchema.parse(decision)) };
21
+ }
22
+ // Serialize instances targeting the same resolved path within this process.
23
+ const queues = new Map();
24
+ function enqueue(path, task) {
25
+ const result = (queues.get(path) ?? Promise.resolve()).catch(() => { }).then(task);
26
+ queues.set(path, result);
27
+ void result.then(() => { if (queues.get(path) === result)
28
+ queues.delete(path); }, () => { if (queues.get(path) === result)
29
+ queues.delete(path); });
30
+ return result;
31
+ }
32
+ /** Single-process JSONL writer. Each append is flushed before it resolves. */
33
+ export function createJsonlDecisionStore(filePath) {
34
+ const path = resolve(filePath);
35
+ return {
36
+ append(decision) {
37
+ // Serialize immediately so callers cannot change a queued record.
38
+ const line = JSON.stringify(guardDecisionSchema.parse(decision)) + '\n';
39
+ return enqueue(path, async () => {
40
+ await mkdir(dirname(path), { recursive: true });
41
+ const file = await open(path, 'a+', 0o600);
42
+ try {
43
+ const { size } = await file.stat();
44
+ if (size > 0) {
45
+ const tail = Buffer.alloc(1);
46
+ await file.read(tail, 0, 1, size - 1);
47
+ if (tail[0] !== 10)
48
+ throw new Error('Incomplete decision log record');
49
+ }
50
+ await file.writeFile(line, 'utf8');
51
+ await file.sync();
52
+ }
53
+ finally {
54
+ await file.close();
55
+ }
56
+ });
57
+ },
58
+ read() {
59
+ return enqueue(path, async () => {
60
+ let contents;
61
+ try {
62
+ contents = await readFile(path, 'utf8');
63
+ }
64
+ catch (error) {
65
+ if (error.code === 'ENOENT')
66
+ return [];
67
+ throw error;
68
+ }
69
+ if (!contents)
70
+ return [];
71
+ if (!contents.endsWith('\n'))
72
+ throw new Error('Incomplete decision log record');
73
+ return contents.slice(0, -1).split('\n').map((line, index) => {
74
+ try {
75
+ return guardDecisionSchema.parse(JSON.parse(line));
76
+ }
77
+ catch {
78
+ throw new Error(`Invalid decision log record at line ${index + 1}`);
79
+ }
80
+ });
81
+ });
82
+ },
83
+ };
84
+ }
@@ -1,5 +1,5 @@
1
1
  export type GuardAction = 'allow' | 'block' | 'redact' | 'approval-required';
2
- export type GuardReasonCode = 'INJECTION_PATTERN' | 'INJECTION_CLEAR' | 'INTENT_ALLOWED' | 'INTENT_REJECTED' | 'PII_DETECTED' | 'PII_CLEAR' | 'TOOL_ALLOWED' | 'TOOL_BLOCKED' | 'TOOL_REDACTED' | 'TOOL_APPROVAL_REQUIRED' | 'TOOL_APPROVED' | 'TOOL_APPROVAL_DENIED' | 'TOOL_POLICY_ERROR';
2
+ export type GuardReasonCode = 'INJECTION_PATTERN' | 'INJECTION_CLEAR' | 'INTENT_ALLOWED' | 'INTENT_REJECTED' | 'PII_DETECTED' | 'PII_CLEAR' | 'TOOL_ALLOWED' | 'TOOL_BLOCKED' | 'TOOL_REDACTED' | 'TOOL_APPROVAL_REQUIRED' | 'TOOL_APPROVED' | 'TOOL_APPROVAL_DENIED' | 'TOOL_POLICY_ERROR' | 'MODEL_FALLBACK_USED' | 'MODEL_UNAVAILABLE';
3
3
  /** Content-free audit contract. Version independently of the npm package. */
4
4
  export interface GuardDecision {
5
5
  schemaVersion: '1';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export { createGuardDecisionStore, createJsonlDecisionStore, guardDecisionSchema } from './core/decision-storage.js';
2
+ export type { GuardDecisionStore, JsonlDecisionStore } from './core/decision-storage.js';
3
+ export { GuardModelError } from './util/fallback.js';
1
4
  export * from './core/decision.js';
2
5
  import type { GuardConfig } from './middleware/middleware.js';
3
6
  export { defineGuardConfig } from './guard.config.js';
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ export { createGuardDecisionStore, createJsonlDecisionStore, guardDecisionSchema } from './core/decision-storage.js';
2
+ export { GuardModelError } from './util/fallback.js';
3
+ import { runGuardModel } from './util/fallback.js';
1
4
  export * from './core/decision.js';
2
5
  import { resolveGuardModels } from './guard.config.js';
3
6
  export { defineGuardConfig } from './guard.config.js';
@@ -30,10 +33,10 @@ function logGuardEvent(eventName, body, attributes = {}) {
30
33
  export async function initGuard(config) {
31
34
  logGuardEvent('guard.models.loading', 'Loading local guard models');
32
35
  const { extractor, pii, mode: piiMode } = resolveGuardModels(config);
33
- const tasks = [ModelSingleton.getExtractor(extractor)];
34
- tasks.push(piiMode === 'ner'
35
- ? ModelSingleton.getNER(pii)
36
- : ModelSingleton.getPIIClassifier(pii));
36
+ const loadPii = (model, mode) => mode === 'ner'
37
+ ? ModelSingleton.getNER(model) : ModelSingleton.getPIIClassifier(model);
38
+ const tasks = [runGuardModel(config, 'intent', () => ModelSingleton.getExtractor(extractor), config?.models?.extractorFallback ? () => ModelSingleton.getExtractor(config.models.extractorFallback) : undefined)];
39
+ tasks.push(runGuardModel(config, 'pii', () => loadPii(pii, piiMode), config?.pii?.fallback ? () => loadPii(config.pii.fallback.model, config.pii.fallback.mode ?? piiMode) : undefined));
37
40
  await Promise.all(tasks);
38
41
  logGuardEvent('guard.models.loaded', 'Local guard models loaded', {
39
42
  piiMode,