@driftgard/node 1.15.0 → 1.16.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 +82 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +81 -1
- package/dist/integrations/langchain.d.ts +50 -0
- package/dist/integrations/langchain.js +77 -0
- package/dist/local-semantic/index.d.ts +79 -0
- package/dist/local-semantic/index.js +239 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,12 @@ const result = await dg.evaluate({
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
if (result.evaluation.allowed) {
|
|
28
|
-
|
|
28
|
+
// If sanitized, use the safe version (PII/secrets redacted)
|
|
29
|
+
if (result.evaluation.sanitized) {
|
|
30
|
+
console.log("Use sanitized:", result.evaluation.sanitized_response);
|
|
31
|
+
} else {
|
|
32
|
+
console.log("Safe to return to user");
|
|
33
|
+
}
|
|
29
34
|
} else {
|
|
30
35
|
// Use the fallback message if configured in your control pack
|
|
31
36
|
if (result.fallback) {
|
|
@@ -123,6 +128,48 @@ const dg = new Driftgard({
|
|
|
123
128
|
| `local` | Control pack fetch only (on init) | Maximum privacy — mental health, clinical, sovereign |
|
|
124
129
|
| `local-with-audit` | Control pack fetch + verdict metadata | Privacy with compliance reporting — healthcare, regulated |
|
|
125
130
|
|
|
131
|
+
### Local semantic matching (ONNX)
|
|
132
|
+
|
|
133
|
+
By default, local mode only runs Layer 1 (pattern matching). Enable `localSemantic: true` to add Layer 2 (semantic similarity) locally via a quantized ONNX model — no data leaves your environment.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Install required dependencies
|
|
137
|
+
npm install onnxruntime-node @xenova/transformers
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const dg = new Driftgard({
|
|
142
|
+
apiKey: process.env.DRIFTGARD_API_KEY,
|
|
143
|
+
mode: "local",
|
|
144
|
+
projectId: "your-project-id",
|
|
145
|
+
localSemantic: true, // enables ONNX-based semantic matching
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// First init() downloads the model (~22MB) and caches it
|
|
149
|
+
await dg.init();
|
|
150
|
+
|
|
151
|
+
// Evaluations now run both Layer 1 (patterns) and Layer 2 (semantic)
|
|
152
|
+
const result = await dg.evaluate({ ... });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The model is downloaded once to `node_modules/.cache/driftgard/` and reused on subsequent runs. You can also provide a custom path:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const dg = new Driftgard({
|
|
159
|
+
...
|
|
160
|
+
localSemantic: true,
|
|
161
|
+
localSemanticModelPath: "/path/to/your/model.onnx",
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| Feature | `localSemantic: false` (default) | `localSemantic: true` |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| Pattern matching (Layer 1) | ✓ | ✓ |
|
|
168
|
+
| Semantic matching (Layer 2) | ✗ | ✓ |
|
|
169
|
+
| Model size | 0 | ~22MB (one-time download) |
|
|
170
|
+
| Extra dependencies | None | `onnxruntime-node`, `@xenova/transformers` |
|
|
171
|
+
| Coverage vs remote | ~60% | ~80% |
|
|
172
|
+
|
|
126
173
|
## Conversation tracking
|
|
127
174
|
|
|
128
175
|
Link evaluations within an agent session using `session_id` and `parent_evaluation_id`:
|
|
@@ -400,6 +447,40 @@ try {
|
|
|
400
447
|
}
|
|
401
448
|
```
|
|
402
449
|
|
|
450
|
+
## Framework Integrations
|
|
451
|
+
|
|
452
|
+
### LangChain.js
|
|
453
|
+
|
|
454
|
+
Use `DriftGardGuardrail` as a step in your LangChain chain. It evaluates the LLM output against your control pack and either passes it through, returns a sanitized version, or throws/returns a fallback on block.
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { DriftGardGuardrail } from "@driftgard/node";
|
|
458
|
+
|
|
459
|
+
const guardrail = new DriftGardGuardrail({
|
|
460
|
+
apiKey: process.env.DRIFTGARD_API_KEY,
|
|
461
|
+
projectId: "your-project-id",
|
|
462
|
+
modelId: "langchain", // optional — for tracking
|
|
463
|
+
onBlock: "raise", // "raise" (throw) or "fallback" (return fallback message)
|
|
464
|
+
failOpen: true, // allow through if DriftGard unreachable
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Use in a LangChain chain via pipe:
|
|
468
|
+
const chain = prompt.pipe(llm).pipe(guardrail).pipe(outputParser);
|
|
469
|
+
|
|
470
|
+
// Or standalone:
|
|
471
|
+
try {
|
|
472
|
+
const safe = await guardrail.invoke("AI response text here");
|
|
473
|
+
console.log(safe); // original text, or sanitized version if redaction applied
|
|
474
|
+
} catch (e) {
|
|
475
|
+
console.log("Blocked:", e.message);
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The guardrail handles three outcomes:
|
|
480
|
+
- Allowed: returns the original text unchanged
|
|
481
|
+
- Sanitized: returns `sanitized_response` (PII/patterns redacted with `[REDACTED]`)
|
|
482
|
+
- Blocked: throws an error (or returns fallback message if `onBlock: "fallback"`)
|
|
483
|
+
|
|
403
484
|
## Requirements
|
|
404
485
|
|
|
405
486
|
- Node.js 18+ (uses native `fetch`)
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { Violation, EvaluationResult, FallbackResponse, HitlInfo } from "./types
|
|
|
4
4
|
export { DriftgardError, AuthError, RateLimitError, FeatureNotAvailableError, ChainDepthExceededError, OrgSuspendedError } from "./errors";
|
|
5
5
|
export { evaluateLocal } from "./local-evaluator";
|
|
6
6
|
export { ControlPackCache } from "./control-pack-cache";
|
|
7
|
+
export { DriftGardGuardrail } from "./integrations/langchain";
|
|
8
|
+
export { LocalSemanticEngine } from "./local-semantic";
|
|
7
9
|
type CircuitState = "closed" | "open" | "half-open";
|
|
8
10
|
export declare class Driftgard {
|
|
9
11
|
private apiKey;
|
|
@@ -18,6 +20,9 @@ export declare class Driftgard {
|
|
|
18
20
|
private cbFailures;
|
|
19
21
|
private cbOpenedAt;
|
|
20
22
|
private cpCache;
|
|
23
|
+
private semanticEngine;
|
|
24
|
+
private localSemantic;
|
|
25
|
+
private localSemanticModelPath?;
|
|
21
26
|
private initialized;
|
|
22
27
|
constructor(config: DriftgardConfig);
|
|
23
28
|
/**
|
|
@@ -26,6 +31,11 @@ export declare class Driftgard {
|
|
|
26
31
|
* No-op for remote mode.
|
|
27
32
|
*/
|
|
28
33
|
init(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Ensure the ONNX model is downloaded. Auto-downloads on first use.
|
|
36
|
+
* Stores in node_modules/.cache/driftgard/
|
|
37
|
+
*/
|
|
38
|
+
private ensureModel;
|
|
29
39
|
/**
|
|
30
40
|
* Stop background refresh and clean up resources.
|
|
31
41
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Driftgard = exports.ControlPackCache = exports.evaluateLocal = exports.OrgSuspendedError = exports.ChainDepthExceededError = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
|
|
3
|
+
exports.Driftgard = exports.LocalSemanticEngine = exports.DriftGardGuardrail = exports.ControlPackCache = exports.evaluateLocal = exports.OrgSuspendedError = exports.ChainDepthExceededError = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
5
|
const local_evaluator_1 = require("./local-evaluator");
|
|
6
6
|
const control_pack_cache_1 = require("./control-pack-cache");
|
|
@@ -15,6 +15,10 @@ var local_evaluator_2 = require("./local-evaluator");
|
|
|
15
15
|
Object.defineProperty(exports, "evaluateLocal", { enumerable: true, get: function () { return local_evaluator_2.evaluateLocal; } });
|
|
16
16
|
var control_pack_cache_2 = require("./control-pack-cache");
|
|
17
17
|
Object.defineProperty(exports, "ControlPackCache", { enumerable: true, get: function () { return control_pack_cache_2.ControlPackCache; } });
|
|
18
|
+
var langchain_1 = require("./integrations/langchain");
|
|
19
|
+
Object.defineProperty(exports, "DriftGardGuardrail", { enumerable: true, get: function () { return langchain_1.DriftGardGuardrail; } });
|
|
20
|
+
var local_semantic_1 = require("./local-semantic");
|
|
21
|
+
Object.defineProperty(exports, "LocalSemanticEngine", { enumerable: true, get: function () { return local_semantic_1.LocalSemanticEngine; } });
|
|
18
22
|
const DEFAULT_BASE_URL = "https://api.driftgard.com";
|
|
19
23
|
const DEFAULT_TIMEOUT = 30000;
|
|
20
24
|
const DEFAULT_MAX_RETRIES = 2;
|
|
@@ -28,6 +32,7 @@ class Driftgard {
|
|
|
28
32
|
this.cbOpenedAt = 0;
|
|
29
33
|
// Local mode
|
|
30
34
|
this.cpCache = null;
|
|
35
|
+
this.semanticEngine = null;
|
|
31
36
|
this.initialized = false;
|
|
32
37
|
if (!config.apiKey)
|
|
33
38
|
throw new Error("apiKey is required");
|
|
@@ -37,6 +42,8 @@ class Driftgard {
|
|
|
37
42
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
38
43
|
this.failureMode = config.failureMode ?? "open";
|
|
39
44
|
this.mode = config.mode ?? "remote";
|
|
45
|
+
this.localSemantic = config.localSemantic ?? false;
|
|
46
|
+
this.localSemanticModelPath = config.localSemanticModelPath;
|
|
40
47
|
this.cbThreshold = config.circuitBreaker?.threshold ?? DEFAULT_CB_THRESHOLD;
|
|
41
48
|
this.cbResetMs = config.circuitBreaker?.resetTimeoutMs ?? DEFAULT_CB_RESET_MS;
|
|
42
49
|
if (this.mode !== "remote") {
|
|
@@ -61,8 +68,44 @@ class Driftgard {
|
|
|
61
68
|
if (this.cpCache) {
|
|
62
69
|
await this.cpCache.init();
|
|
63
70
|
}
|
|
71
|
+
// Load local semantic engine if enabled
|
|
72
|
+
if (this.mode !== "remote" && this.localSemantic) {
|
|
73
|
+
try {
|
|
74
|
+
const { LocalSemanticEngine } = require("./local-semantic");
|
|
75
|
+
const modelPath = this.localSemanticModelPath || await this.ensureModel();
|
|
76
|
+
this.semanticEngine = new LocalSemanticEngine({ modelPath });
|
|
77
|
+
await this.semanticEngine.init();
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
console.warn(`[driftgard] Local semantic engine not available: ${e.message}`);
|
|
81
|
+
this.semanticEngine = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
64
84
|
this.initialized = true;
|
|
65
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Ensure the ONNX model is downloaded. Auto-downloads on first use.
|
|
88
|
+
* Stores in node_modules/.cache/driftgard/
|
|
89
|
+
*/
|
|
90
|
+
async ensureModel() {
|
|
91
|
+
const fs = require("fs");
|
|
92
|
+
const path = require("path");
|
|
93
|
+
const cacheDir = path.join(process.cwd(), "node_modules", ".cache", "driftgard");
|
|
94
|
+
const modelFile = path.join(cacheDir, "all-MiniLM-L6-v2-quantized.onnx");
|
|
95
|
+
if (fs.existsSync(modelFile))
|
|
96
|
+
return modelFile;
|
|
97
|
+
// Download from DriftGard CDN
|
|
98
|
+
const MODEL_URL = "https://cdn.driftgard.com/models/all-MiniLM-L6-v2-quantized.onnx";
|
|
99
|
+
console.log(`[driftgard] Downloading semantic model (~22MB)...`);
|
|
100
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
101
|
+
const res = await fetch(MODEL_URL);
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new Error(`Model download failed: ${res.status}`);
|
|
104
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
105
|
+
fs.writeFileSync(modelFile, buffer);
|
|
106
|
+
console.log(`[driftgard] Model downloaded to ${modelFile}`);
|
|
107
|
+
return modelFile;
|
|
108
|
+
}
|
|
66
109
|
/**
|
|
67
110
|
* Stop background refresh and clean up resources.
|
|
68
111
|
*/
|
|
@@ -109,6 +152,43 @@ class Driftgard {
|
|
|
109
152
|
...(req.jurisdiction ? { jurisdiction: req.jurisdiction } : {}),
|
|
110
153
|
};
|
|
111
154
|
const verdict = (0, local_evaluator_1.evaluateLocal)(cp, wasmRequest);
|
|
155
|
+
// Run local semantic matching if engine is available
|
|
156
|
+
if (this.semanticEngine?.isReady()) {
|
|
157
|
+
try {
|
|
158
|
+
const semanticMatches = await this.semanticEngine.semanticMatch(req.prompt || "", req.response || "", cp.policy_rules || [], { defaultThreshold: 0.55 });
|
|
159
|
+
// Merge semantic matches into verdict (avoid duplicates by clause_id)
|
|
160
|
+
const existingClauses = new Set(verdict.violations.map((v) => v.clause_id));
|
|
161
|
+
for (const sm of semanticMatches) {
|
|
162
|
+
if (!existingClauses.has(sm.clause_id)) {
|
|
163
|
+
verdict.violations.push({
|
|
164
|
+
clause_id: sm.clause_id,
|
|
165
|
+
severity: sm.severity,
|
|
166
|
+
category: sm.category,
|
|
167
|
+
reason: sm.reason,
|
|
168
|
+
});
|
|
169
|
+
existingClauses.add(sm.clause_id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Recalculate risk score with semantic matches
|
|
173
|
+
if (semanticMatches.length > 0) {
|
|
174
|
+
const weights = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
175
|
+
for (const sm of semanticMatches) {
|
|
176
|
+
if (!existingClauses.has(sm.clause_id))
|
|
177
|
+
continue; // already counted
|
|
178
|
+
verdict.risk_score += weights[sm.severity] || 5;
|
|
179
|
+
}
|
|
180
|
+
// Check if should block
|
|
181
|
+
const blockThreshold = cp.risk_scoring?.block_threshold || 8;
|
|
182
|
+
if (verdict.risk_score >= blockThreshold) {
|
|
183
|
+
verdict.allowed = false;
|
|
184
|
+
verdict.flags.action_blocked = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Semantic engine failure is non-fatal — Layer 1 results still valid
|
|
190
|
+
}
|
|
191
|
+
}
|
|
112
192
|
const response = {
|
|
113
193
|
ok: true,
|
|
114
194
|
project_id: req.project_id,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DriftGard LangChain.js Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides a guardrail that evaluates LLM outputs against your DriftGard control pack.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { DriftGardGuardrail } from '@driftgard/sdk/integrations/langchain';
|
|
8
|
+
*
|
|
9
|
+
* const guardrail = new DriftGardGuardrail({
|
|
10
|
+
* apiKey: 'dg_...',
|
|
11
|
+
* projectId: 'proj_xxx',
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Use in a chain:
|
|
15
|
+
* const chain = prompt.pipe(llm).pipe(guardrail).pipe(outputParser);
|
|
16
|
+
*
|
|
17
|
+
* // Or standalone:
|
|
18
|
+
* const safe = await guardrail.invoke("unsafe AI response");
|
|
19
|
+
*/
|
|
20
|
+
export interface DriftGardGuardrailConfig {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
projectId: string;
|
|
23
|
+
modelId?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
/** "raise" (throw on block) or "fallback" (return fallback message). Default "raise". */
|
|
26
|
+
onBlock?: "raise" | "fallback";
|
|
27
|
+
/** Allow through if DriftGard is unreachable. Default true. */
|
|
28
|
+
failOpen?: boolean;
|
|
29
|
+
/** Optional prompt to send alongside the response for context-aware evaluation. */
|
|
30
|
+
prompt?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class DriftGardGuardrail {
|
|
33
|
+
private client;
|
|
34
|
+
private projectId;
|
|
35
|
+
private modelId;
|
|
36
|
+
private onBlock;
|
|
37
|
+
private prompt;
|
|
38
|
+
constructor(config: DriftGardGuardrailConfig);
|
|
39
|
+
/**
|
|
40
|
+
* Evaluate the input and return safe output.
|
|
41
|
+
* Throws if blocked (when onBlock="raise").
|
|
42
|
+
*/
|
|
43
|
+
invoke(input: string | any): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* LangChain Runnable interface — pipe support.
|
|
46
|
+
*/
|
|
47
|
+
pipe(next: any): {
|
|
48
|
+
invoke(input: any): Promise<any>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DriftGard LangChain.js Integration
|
|
4
|
+
*
|
|
5
|
+
* Provides a guardrail that evaluates LLM outputs against your DriftGard control pack.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { DriftGardGuardrail } from '@driftgard/sdk/integrations/langchain';
|
|
9
|
+
*
|
|
10
|
+
* const guardrail = new DriftGardGuardrail({
|
|
11
|
+
* apiKey: 'dg_...',
|
|
12
|
+
* projectId: 'proj_xxx',
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // Use in a chain:
|
|
16
|
+
* const chain = prompt.pipe(llm).pipe(guardrail).pipe(outputParser);
|
|
17
|
+
*
|
|
18
|
+
* // Or standalone:
|
|
19
|
+
* const safe = await guardrail.invoke("unsafe AI response");
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.DriftGardGuardrail = void 0;
|
|
23
|
+
const index_1 = require("../index");
|
|
24
|
+
class DriftGardGuardrail {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.projectId = config.projectId;
|
|
27
|
+
this.modelId = config.modelId || "langchain";
|
|
28
|
+
this.onBlock = config.onBlock || "raise";
|
|
29
|
+
this.prompt = config.prompt || "";
|
|
30
|
+
this.client = new index_1.Driftgard({
|
|
31
|
+
apiKey: config.apiKey,
|
|
32
|
+
baseUrl: config.baseUrl,
|
|
33
|
+
projectId: config.projectId,
|
|
34
|
+
failureMode: config.failOpen !== false ? "open" : "closed",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Evaluate the input and return safe output.
|
|
39
|
+
* Throws if blocked (when onBlock="raise").
|
|
40
|
+
*/
|
|
41
|
+
async invoke(input) {
|
|
42
|
+
const text = typeof input === "string" ? input : String(input?.content || input?.text || input);
|
|
43
|
+
const prompt = typeof input === "object" && input?.prompt ? String(input.prompt) : this.prompt;
|
|
44
|
+
const result = await this.client.evaluate({
|
|
45
|
+
project_id: this.projectId,
|
|
46
|
+
prompt,
|
|
47
|
+
response: text,
|
|
48
|
+
model_id: this.modelId,
|
|
49
|
+
});
|
|
50
|
+
const evaluation = result.evaluation;
|
|
51
|
+
if (!evaluation.allowed) {
|
|
52
|
+
const fallbackMsg = result.fallback?.message || "Response blocked by policy.";
|
|
53
|
+
if (this.onBlock === "raise") {
|
|
54
|
+
throw new Error(`[DriftGard] Blocked: ${fallbackMsg}`);
|
|
55
|
+
}
|
|
56
|
+
return fallbackMsg;
|
|
57
|
+
}
|
|
58
|
+
// If sanitized, return the safe version
|
|
59
|
+
if (evaluation.sanitized && evaluation.sanitized_response) {
|
|
60
|
+
return evaluation.sanitized_response;
|
|
61
|
+
}
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* LangChain Runnable interface — pipe support.
|
|
66
|
+
*/
|
|
67
|
+
pipe(next) {
|
|
68
|
+
const self = this;
|
|
69
|
+
return {
|
|
70
|
+
async invoke(input) {
|
|
71
|
+
const mid = await self.invoke(input);
|
|
72
|
+
return next?.invoke ? next.invoke(mid) : mid;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.DriftGardGuardrail = DriftGardGuardrail;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DriftGard Local Semantic — ONNX-based embedding for local evaluation mode.
|
|
3
|
+
*
|
|
4
|
+
* Provides semantic matching without any external API calls.
|
|
5
|
+
* Uses a quantized all-MiniLM-L6-v2 model (~22MB) via ONNX Runtime.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { LocalSemanticEngine } from "@driftgard/node/local-semantic";
|
|
9
|
+
*
|
|
10
|
+
* const engine = new LocalSemanticEngine();
|
|
11
|
+
* await engine.init();
|
|
12
|
+
*
|
|
13
|
+
* const matches = await engine.semanticMatch(text, rules, { defaultThreshold: 0.55 });
|
|
14
|
+
*/
|
|
15
|
+
export interface SemanticMatch {
|
|
16
|
+
clause_id: string;
|
|
17
|
+
category: string;
|
|
18
|
+
severity: string;
|
|
19
|
+
action: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
confidence: number;
|
|
22
|
+
source: "semantic";
|
|
23
|
+
match_target?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface PolicyRule {
|
|
26
|
+
clause_id: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
category?: string;
|
|
29
|
+
severity?: string;
|
|
30
|
+
action?: string;
|
|
31
|
+
pattern_rules?: string[];
|
|
32
|
+
match_target?: string;
|
|
33
|
+
negation_aware?: boolean;
|
|
34
|
+
semantic_threshold?: number;
|
|
35
|
+
jurisdictions?: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface SemanticMatchOptions {
|
|
38
|
+
defaultThreshold?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare class LocalSemanticEngine {
|
|
41
|
+
private session;
|
|
42
|
+
private tokenizer;
|
|
43
|
+
private ready;
|
|
44
|
+
private modelPath;
|
|
45
|
+
constructor(opts?: {
|
|
46
|
+
modelPath?: string;
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the ONNX runtime and load the model.
|
|
50
|
+
* Call once on startup.
|
|
51
|
+
*/
|
|
52
|
+
init(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Check if the engine is ready.
|
|
55
|
+
*/
|
|
56
|
+
isReady(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Generate embedding for a text string.
|
|
59
|
+
* Returns a normalized Float32Array (384 dimensions for MiniLM-L6).
|
|
60
|
+
*/
|
|
61
|
+
embed(text: string): Promise<Float32Array>;
|
|
62
|
+
/**
|
|
63
|
+
* Compute cosine similarity between two embeddings.
|
|
64
|
+
*/
|
|
65
|
+
cosineSimilarity(a: Float32Array, b: Float32Array): number;
|
|
66
|
+
/**
|
|
67
|
+
* Run semantic matching against policy rules.
|
|
68
|
+
* Same logic as server-side semantic.js but runs locally.
|
|
69
|
+
*/
|
|
70
|
+
semanticMatch(prompt: string, response: string, policyRules: PolicyRule[], opts?: SemanticMatchOptions): Promise<SemanticMatch[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Extract searchable texts from a rule (patterns + description).
|
|
73
|
+
*/
|
|
74
|
+
private getRuleTexts;
|
|
75
|
+
/**
|
|
76
|
+
* Clean up resources.
|
|
77
|
+
*/
|
|
78
|
+
destroy(): void;
|
|
79
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DriftGard Local Semantic — ONNX-based embedding for local evaluation mode.
|
|
4
|
+
*
|
|
5
|
+
* Provides semantic matching without any external API calls.
|
|
6
|
+
* Uses a quantized all-MiniLM-L6-v2 model (~22MB) via ONNX Runtime.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { LocalSemanticEngine } from "@driftgard/node/local-semantic";
|
|
10
|
+
*
|
|
11
|
+
* const engine = new LocalSemanticEngine();
|
|
12
|
+
* await engine.init();
|
|
13
|
+
*
|
|
14
|
+
* const matches = await engine.semanticMatch(text, rules, { defaultThreshold: 0.55 });
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.LocalSemanticEngine = void 0;
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
// Embedding cache for rule patterns (keyed by CP id + version)
|
|
53
|
+
const ruleEmbeddingCache = new Map();
|
|
54
|
+
class LocalSemanticEngine {
|
|
55
|
+
constructor(opts) {
|
|
56
|
+
this.session = null;
|
|
57
|
+
this.tokenizer = null;
|
|
58
|
+
this.ready = false;
|
|
59
|
+
this.modelPath = opts?.modelPath || path.join(__dirname, "model", "all-MiniLM-L6-v2-quantized.onnx");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the ONNX runtime and load the model.
|
|
63
|
+
* Call once on startup.
|
|
64
|
+
*/
|
|
65
|
+
async init() {
|
|
66
|
+
if (this.ready)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const ort = require("onnxruntime-node");
|
|
70
|
+
this.session = await ort.InferenceSession.create(this.modelPath, {
|
|
71
|
+
executionProviders: ["cpu"],
|
|
72
|
+
graphOptimizationLevel: "all",
|
|
73
|
+
});
|
|
74
|
+
// Load tokenizer
|
|
75
|
+
const { AutoTokenizer } = require("@xenova/transformers");
|
|
76
|
+
this.tokenizer = await AutoTokenizer.from_pretrained("Xenova/all-MiniLM-L6-v2");
|
|
77
|
+
this.ready = true;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
throw new Error(`Failed to initialize local semantic engine: ${e.message}. ` +
|
|
81
|
+
`Ensure 'onnxruntime-node' and '@xenova/transformers' are installed: ` +
|
|
82
|
+
`npm install onnxruntime-node @xenova/transformers`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if the engine is ready.
|
|
87
|
+
*/
|
|
88
|
+
isReady() {
|
|
89
|
+
return this.ready;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generate embedding for a text string.
|
|
93
|
+
* Returns a normalized Float32Array (384 dimensions for MiniLM-L6).
|
|
94
|
+
*/
|
|
95
|
+
async embed(text) {
|
|
96
|
+
if (!this.ready)
|
|
97
|
+
throw new Error("Engine not initialized. Call init() first.");
|
|
98
|
+
// Tokenize
|
|
99
|
+
const encoded = await this.tokenizer(text, {
|
|
100
|
+
padding: true,
|
|
101
|
+
truncation: true,
|
|
102
|
+
max_length: 128,
|
|
103
|
+
});
|
|
104
|
+
const inputIds = encoded.input_ids.data;
|
|
105
|
+
const attentionMask = encoded.attention_mask.data;
|
|
106
|
+
const ort = require("onnxruntime-node");
|
|
107
|
+
// Create tensors
|
|
108
|
+
const inputIdsTensor = new ort.Tensor("int64", BigInt64Array.from(inputIds.map((v) => BigInt(v))), [1, inputIds.length]);
|
|
109
|
+
const attentionMaskTensor = new ort.Tensor("int64", BigInt64Array.from(attentionMask.map((v) => BigInt(v))), [1, attentionMask.length]);
|
|
110
|
+
const tokenTypeIds = new ort.Tensor("int64", new BigInt64Array(inputIds.length).fill(0n), [1, inputIds.length]);
|
|
111
|
+
// Run inference
|
|
112
|
+
const results = await this.session.run({
|
|
113
|
+
input_ids: inputIdsTensor,
|
|
114
|
+
attention_mask: attentionMaskTensor,
|
|
115
|
+
token_type_ids: tokenTypeIds,
|
|
116
|
+
});
|
|
117
|
+
// Mean pooling over token embeddings (masked)
|
|
118
|
+
const output = results["last_hidden_state"] || results[Object.keys(results)[0]];
|
|
119
|
+
const data = output.data;
|
|
120
|
+
const seqLen = inputIds.length;
|
|
121
|
+
const hiddenSize = data.length / seqLen;
|
|
122
|
+
const pooled = new Float32Array(hiddenSize);
|
|
123
|
+
let maskSum = 0;
|
|
124
|
+
for (let i = 0; i < seqLen; i++) {
|
|
125
|
+
if (attentionMask[i] === 1) {
|
|
126
|
+
maskSum++;
|
|
127
|
+
for (let j = 0; j < hiddenSize; j++) {
|
|
128
|
+
pooled[j] += data[i * hiddenSize + j];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (let j = 0; j < hiddenSize; j++) {
|
|
133
|
+
pooled[j] /= maskSum;
|
|
134
|
+
}
|
|
135
|
+
// L2 normalize
|
|
136
|
+
let norm = 0;
|
|
137
|
+
for (let j = 0; j < hiddenSize; j++)
|
|
138
|
+
norm += pooled[j] * pooled[j];
|
|
139
|
+
norm = Math.sqrt(norm);
|
|
140
|
+
for (let j = 0; j < hiddenSize; j++)
|
|
141
|
+
pooled[j] /= norm;
|
|
142
|
+
return pooled;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Compute cosine similarity between two embeddings.
|
|
146
|
+
*/
|
|
147
|
+
cosineSimilarity(a, b) {
|
|
148
|
+
let dot = 0;
|
|
149
|
+
for (let i = 0; i < a.length; i++)
|
|
150
|
+
dot += a[i] * b[i];
|
|
151
|
+
return dot; // already normalized, so dot product = cosine similarity
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run semantic matching against policy rules.
|
|
155
|
+
* Same logic as server-side semantic.js but runs locally.
|
|
156
|
+
*/
|
|
157
|
+
async semanticMatch(prompt, response, policyRules, opts = {}) {
|
|
158
|
+
if (!this.ready)
|
|
159
|
+
return [];
|
|
160
|
+
if (!policyRules?.length)
|
|
161
|
+
return [];
|
|
162
|
+
const defaultThreshold = opts.defaultThreshold ?? 0.55;
|
|
163
|
+
const matches = [];
|
|
164
|
+
// Determine text to embed based on match_target
|
|
165
|
+
const textVariants = {
|
|
166
|
+
prompt: prompt || "",
|
|
167
|
+
response: response || "",
|
|
168
|
+
both: [prompt, response].filter(Boolean).join(" "),
|
|
169
|
+
};
|
|
170
|
+
// Embed text variants (cached per call)
|
|
171
|
+
const textEmbeddings = new Map();
|
|
172
|
+
for (const rule of policyRules) {
|
|
173
|
+
const severity = String(rule.severity || "medium").toLowerCase();
|
|
174
|
+
const hardThreshold = rule.semantic_threshold != null
|
|
175
|
+
? rule.semantic_threshold
|
|
176
|
+
: severity === "critical" ? 0.48
|
|
177
|
+
: severity === "high" ? 0.53
|
|
178
|
+
: severity === "low" ? 0.58
|
|
179
|
+
: defaultThreshold;
|
|
180
|
+
const matchTarget = String(rule.match_target || "both").toLowerCase();
|
|
181
|
+
const text = textVariants[matchTarget] || textVariants.both;
|
|
182
|
+
if (!text)
|
|
183
|
+
continue;
|
|
184
|
+
// Get or compute text embedding
|
|
185
|
+
if (!textEmbeddings.has(text)) {
|
|
186
|
+
textEmbeddings.set(text, await this.embed(text));
|
|
187
|
+
}
|
|
188
|
+
const textEmb = textEmbeddings.get(text);
|
|
189
|
+
// Get rule embeddings (from patterns + description)
|
|
190
|
+
const ruleTexts = this.getRuleTexts(rule);
|
|
191
|
+
let bestSimilarity = 0;
|
|
192
|
+
let bestPatternText = "";
|
|
193
|
+
for (const ruleText of ruleTexts) {
|
|
194
|
+
const ruleEmb = await this.embed(ruleText);
|
|
195
|
+
const sim = this.cosineSimilarity(textEmb, ruleEmb);
|
|
196
|
+
if (sim > bestSimilarity) {
|
|
197
|
+
bestSimilarity = sim;
|
|
198
|
+
bestPatternText = ruleText;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (bestSimilarity >= hardThreshold) {
|
|
202
|
+
matches.push({
|
|
203
|
+
clause_id: rule.clause_id,
|
|
204
|
+
category: rule.category || "policy_compliance",
|
|
205
|
+
severity: rule.severity || "medium",
|
|
206
|
+
action: rule.action || "flag",
|
|
207
|
+
reason: `Semantic match: ${Math.round(bestSimilarity * 100)}% similar to "${bestPatternText.slice(0, 50)}"`,
|
|
208
|
+
confidence: bestSimilarity,
|
|
209
|
+
source: "semantic",
|
|
210
|
+
match_target: matchTarget,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return matches;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Extract searchable texts from a rule (patterns + description).
|
|
218
|
+
*/
|
|
219
|
+
getRuleTexts(rule) {
|
|
220
|
+
const texts = [];
|
|
221
|
+
if (rule.description)
|
|
222
|
+
texts.push(rule.description);
|
|
223
|
+
for (const p of rule.pattern_rules || []) {
|
|
224
|
+
if (p && p.length > 3)
|
|
225
|
+
texts.push(p);
|
|
226
|
+
}
|
|
227
|
+
return texts.length > 0 ? texts : [""];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Clean up resources.
|
|
231
|
+
*/
|
|
232
|
+
destroy() {
|
|
233
|
+
this.session = null;
|
|
234
|
+
this.tokenizer = null;
|
|
235
|
+
this.ready = false;
|
|
236
|
+
ruleEmbeddingCache.clear();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.LocalSemanticEngine = LocalSemanticEngine;
|
package/dist/types.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ export interface DriftgardConfig {
|
|
|
31
31
|
error?: string;
|
|
32
32
|
stale?: boolean;
|
|
33
33
|
}) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Enable local semantic matching via ONNX (Layer 2).
|
|
36
|
+
* When true, the SDK downloads a quantized embedding model (~22MB) on first init
|
|
37
|
+
* and runs semantic matching locally alongside the WASM pattern engine.
|
|
38
|
+
* Requires: `npm install onnxruntime-node @xenova/transformers`
|
|
39
|
+
* Default: false (Layer 1 pattern matching only in local mode).
|
|
40
|
+
*/
|
|
41
|
+
localSemantic?: boolean;
|
|
42
|
+
/** Custom path to the ONNX model file. If not set, auto-downloads to node_modules/.cache/driftgard/ */
|
|
43
|
+
localSemanticModelPath?: string;
|
|
34
44
|
}
|
|
35
45
|
export interface EvaluateRequest {
|
|
36
46
|
project_id: string;
|
|
@@ -81,6 +91,10 @@ export interface EvaluationResult {
|
|
|
81
91
|
allowed: boolean;
|
|
82
92
|
risk_score: number;
|
|
83
93
|
violations: Violation[];
|
|
94
|
+
/** Whether the response was sanitized (matched content redacted). */
|
|
95
|
+
sanitized?: boolean;
|
|
96
|
+
/** The sanitized version of the response with violations redacted. Only present when sanitized=true. */
|
|
97
|
+
sanitized_response?: string;
|
|
84
98
|
/** Original policy decision before execution_mode override (only present when overridden). */
|
|
85
99
|
policy_allowed?: boolean;
|
|
86
100
|
flags?: {
|
package/package.json
CHANGED