@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
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
|
-
##
|
|
20
|
+
## Full setup guide
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
|
|
43
|
+
# Download MiniLM + OpenAI/privacy-filter
|
|
44
|
+
node node_modules/@intflows/genkit-guard/scripts/download-model.js
|
|
26
45
|
```
|
|
27
46
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
```bash
|
|
129
|
+
export GEMINI_API_KEY="your-api-key"
|
|
130
|
+
export GEMINI_MODEL="your-model-name"
|
|
131
|
+
```
|
|
80
132
|
|
|
81
|
-
|
|
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.
|
|
357
|
+
Console logging emits structured JSON. In v0.1.0, attach a persistent store to your shared configuration:
|
|
223
358
|
|
|
224
359
|
```ts
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/core/decision.d.ts
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|