@vpdeva/blackwall-llm-shield-js 0.1.5 → 0.1.7
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 +61 -7
- package/index.d.ts +104 -0
- package/integrations.d.ts +18 -0
- package/package.json +22 -5
- package/providers.d.ts +6 -0
- package/semantic.d.ts +4 -0
- package/src/index.js +134 -1
- package/src/providers.js +48 -5
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ npm install @xenova/transformers
|
|
|
32
32
|
## Fast Start
|
|
33
33
|
|
|
34
34
|
```js
|
|
35
|
-
const { BlackwallShield } = require('blackwall-llm-shield-js');
|
|
35
|
+
const { BlackwallShield } = require('@vpdeva/blackwall-llm-shield-js');
|
|
36
36
|
|
|
37
37
|
const shield = new BlackwallShield({
|
|
38
38
|
blockOnPromptInjection: true,
|
|
@@ -95,9 +95,9 @@ Use `createExpressMiddleware()`, `createLangChainCallbacks()`, or `createLlamaIn
|
|
|
95
95
|
|
|
96
96
|
### Subpath modules
|
|
97
97
|
|
|
98
|
-
Use `require('blackwall-llm-shield-js/integrations')` for callback wrappers and `require('blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
|
|
98
|
+
Use `require('@vpdeva/blackwall-llm-shield-js/integrations')` for callback wrappers and `require('@vpdeva/blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
|
|
99
99
|
|
|
100
|
-
Use `require('blackwall-llm-shield-js/providers')` for provider adapter factories.
|
|
100
|
+
Use `require('@vpdeva/blackwall-llm-shield-js/providers')` for provider adapter factories.
|
|
101
101
|
|
|
102
102
|
## Core Building Blocks
|
|
103
103
|
|
|
@@ -105,7 +105,7 @@ Use `require('blackwall-llm-shield-js/providers')` for provider adapter factorie
|
|
|
105
105
|
|
|
106
106
|
Use it to sanitize inbound messages, mask sensitive data, score prompt-injection risk, and decide whether the request should continue to the model provider.
|
|
107
107
|
|
|
108
|
-
It also exposes `protectModelCall()`, `protectWithAdapter()`, and `reviewModelResponse()` so you can enforce request checks before provider calls and review outputs before they go back to the user.
|
|
108
|
+
It also exposes `protectModelCall()`, `protectJsonModelCall()`, `protectWithAdapter()`, and `reviewModelResponse()` so you can enforce request checks before provider calls and review outputs before they go back to the user.
|
|
109
109
|
|
|
110
110
|
### `OutputFirewall`
|
|
111
111
|
|
|
@@ -129,6 +129,10 @@ Recommended presets:
|
|
|
129
129
|
- `strict` for high-sensitivity routes
|
|
130
130
|
- `ragSafe` for retrieval-heavy flows
|
|
131
131
|
- `agentTools` for tool-calling and approval-gated agent actions
|
|
132
|
+
- `agentPlanner` for JSON-heavy planner and internal ops routes
|
|
133
|
+
- `documentReview` for classification and document-review pipelines
|
|
134
|
+
- `ragSearch` for search-heavy retrieval endpoints
|
|
135
|
+
- `toolCalling` for routes that broker external actions
|
|
132
136
|
|
|
133
137
|
### `AuditTrail`
|
|
134
138
|
|
|
@@ -151,7 +155,7 @@ if (!guarded.allowed) {
|
|
|
151
155
|
### Wrap a provider call end to end
|
|
152
156
|
|
|
153
157
|
```js
|
|
154
|
-
const { BlackwallShield, createOpenAIAdapter } = require('blackwall-llm-shield-js');
|
|
158
|
+
const { BlackwallShield, createOpenAIAdapter } = require('@vpdeva/blackwall-llm-shield-js');
|
|
155
159
|
|
|
156
160
|
const shield = new BlackwallShield({
|
|
157
161
|
preset: 'shadowFirst',
|
|
@@ -177,6 +181,19 @@ const result = await shield.protectWithAdapter({
|
|
|
177
181
|
console.log(result.stage, result.allowed);
|
|
178
182
|
```
|
|
179
183
|
|
|
184
|
+
### Protect a strict JSON workflow
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
const result = await shield.protectJsonModelCall({
|
|
188
|
+
messages: [{ role: 'user', content: 'Return the shipment triage plan as JSON.' }],
|
|
189
|
+
metadata: { route: '/api/planner', feature: 'planner' },
|
|
190
|
+
requiredSchema: { steps: 'object' },
|
|
191
|
+
callModel: async () => JSON.stringify({ steps: ['triage', 'notify-ops'] }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log(result.json.parsed);
|
|
195
|
+
```
|
|
196
|
+
|
|
180
197
|
### Use presets and route-level policy overrides
|
|
181
198
|
|
|
182
199
|
```js
|
|
@@ -231,18 +248,51 @@ const toolFirewall = new ToolPermissionFirewall({
|
|
|
231
248
|
});
|
|
232
249
|
```
|
|
233
250
|
|
|
251
|
+
For document review and verification:
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
const shield = new BlackwallShield({
|
|
255
|
+
preset: 'documentReview',
|
|
256
|
+
routePolicies: [
|
|
257
|
+
{
|
|
258
|
+
route: '/api/verify',
|
|
259
|
+
options: {
|
|
260
|
+
shadowMode: true,
|
|
261
|
+
outputFirewallDefaults: { requiredSchema: { verdict: 'string' } },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Choose your integration path
|
|
269
|
+
|
|
270
|
+
- Request-only guard: `guardModelRequest()`
|
|
271
|
+
- Request + output review: `protectModelCall()`
|
|
272
|
+
- Strict JSON planner/document workflows: `protectJsonModelCall()`
|
|
273
|
+
- Full provider wrapper: `protectWithAdapter()`
|
|
274
|
+
- Tool firewall + RAG sanitizer: `ToolPermissionFirewall` + `RetrievalSanitizer`
|
|
275
|
+
|
|
234
276
|
### Operational telemetry summaries
|
|
235
277
|
|
|
236
278
|
```js
|
|
279
|
+
const { summarizeOperationalTelemetry } = require('@vpdeva/blackwall-llm-shield-js');
|
|
237
280
|
const summary = summarizeOperationalTelemetry(events);
|
|
238
281
|
console.log(summary.byRoute);
|
|
282
|
+
console.log(summary.byFeature);
|
|
283
|
+
console.log(summary.noisiestRoutes);
|
|
284
|
+
console.log(summary.weeklyBlockEstimate);
|
|
239
285
|
console.log(summary.highestSeverity);
|
|
240
286
|
```
|
|
241
287
|
|
|
288
|
+
### TypeScript
|
|
289
|
+
|
|
290
|
+
The package now ships first-class declaration files for the main entry point plus `integrations`, `providers`, and `semantic` subpaths, so local declaration shims should no longer be necessary in TypeScript apps.
|
|
291
|
+
|
|
242
292
|
### Inspect model output
|
|
243
293
|
|
|
244
294
|
```js
|
|
245
|
-
const { OutputFirewall } = require('blackwall-llm-shield-js');
|
|
295
|
+
const { OutputFirewall } = require('@vpdeva/blackwall-llm-shield-js');
|
|
246
296
|
|
|
247
297
|
const firewall = new OutputFirewall({
|
|
248
298
|
riskThreshold: 'high',
|
|
@@ -259,7 +309,7 @@ console.log(review.allowed);
|
|
|
259
309
|
### Gate tool execution
|
|
260
310
|
|
|
261
311
|
```js
|
|
262
|
-
const { ToolPermissionFirewall } = require('blackwall-llm-shield-js');
|
|
312
|
+
const { ToolPermissionFirewall } = require('@vpdeva/blackwall-llm-shield-js');
|
|
263
313
|
|
|
264
314
|
const tools = new ToolPermissionFirewall({
|
|
265
315
|
allowedTools: ['search', 'lookupCustomer'],
|
|
@@ -274,6 +324,10 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
|
|
|
274
324
|
- [`examples/nextjs-app-router/app/api/chat/route.js`](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/examples/nextjs-app-router/app/api/chat/route.js) shows guarded request handling in a Next.js route
|
|
275
325
|
- [`examples/admin-dashboard/index.html`](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/examples/admin-dashboard/index.html) shows a polished security command center demo
|
|
276
326
|
|
|
327
|
+
For Next.js, the most production-real patterns are App Router route handlers, server actions for trusted internal mutations, and streaming endpoints that apply output review to assembled or final chunks instead of raw intermediate tokens.
|
|
328
|
+
|
|
329
|
+
For Gemini-heavy apps, the bundled adapter now preserves system instructions plus mixed text/image/file parts so App Router handlers can wrap direct `@google/generative-ai` calls with less translation glue.
|
|
330
|
+
|
|
277
331
|
## Release Commands
|
|
278
332
|
|
|
279
333
|
- `npm run release:check` runs the JS test suite before release
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
2
|
+
|
|
3
|
+
export interface MessagePart {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
image_url?: string;
|
|
7
|
+
file_id?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ShieldMessage {
|
|
12
|
+
role: 'system' | 'user' | 'assistant';
|
|
13
|
+
content: string | MessagePart[] | Record<string, unknown>;
|
|
14
|
+
trusted?: boolean;
|
|
15
|
+
contentParts?: MessagePart[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GuardResult {
|
|
19
|
+
allowed: boolean;
|
|
20
|
+
blocked: boolean;
|
|
21
|
+
reason: string | null;
|
|
22
|
+
messages: Array<ShieldMessage & { content: string; contentParts?: MessagePart[] }>;
|
|
23
|
+
report: Record<string, unknown>;
|
|
24
|
+
vault: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ReviewResult {
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
severity: RiskLevel;
|
|
30
|
+
findings: Array<Record<string, unknown>>;
|
|
31
|
+
report: Record<string, unknown>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface JsonProtectionResult extends Record<string, unknown> {
|
|
36
|
+
allowed: boolean;
|
|
37
|
+
blocked: boolean;
|
|
38
|
+
json?: {
|
|
39
|
+
parsed: unknown;
|
|
40
|
+
schemaValid: boolean;
|
|
41
|
+
parseError?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProviderAdapter {
|
|
46
|
+
provider: string;
|
|
47
|
+
invoke(payload: { messages: ShieldMessage[]; metadata?: Record<string, unknown>; guard?: GuardResult }): Promise<unknown> | unknown;
|
|
48
|
+
extractOutput?(response: unknown, request?: GuardResult): unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ShieldOptions {
|
|
52
|
+
preset?: string | null;
|
|
53
|
+
policyPack?: string | null;
|
|
54
|
+
shadowMode?: boolean;
|
|
55
|
+
routePolicies?: Array<{ route: string | RegExp | ((route: string, metadata: Record<string, unknown>) => boolean); options: Record<string, unknown> }>;
|
|
56
|
+
customPromptDetectors?: Array<(payload: Record<string, unknown>) => Record<string, unknown> | Array<Record<string, unknown>> | null>;
|
|
57
|
+
onTelemetry?: (event: Record<string, unknown>) => void | Promise<void>;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class BlackwallShield {
|
|
62
|
+
constructor(options?: ShieldOptions);
|
|
63
|
+
inspectText(text: unknown): Record<string, unknown>;
|
|
64
|
+
guardModelRequest(input?: { messages?: ShieldMessage[]; metadata?: Record<string, unknown>; allowSystemMessages?: boolean; comparePolicyPacks?: string[] }): Promise<GuardResult>;
|
|
65
|
+
reviewModelResponse(input?: { output: unknown; metadata?: Record<string, unknown>; outputFirewall?: OutputFirewall | null; firewallOptions?: Record<string, unknown> }): Promise<ReviewResult>;
|
|
66
|
+
protectModelCall(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
67
|
+
protectJsonModelCall(input: Record<string, unknown>): Promise<JsonProtectionResult>;
|
|
68
|
+
protectWithAdapter(input: { adapter: ProviderAdapter; messages?: ShieldMessage[]; metadata?: Record<string, unknown>; allowSystemMessages?: boolean; comparePolicyPacks?: string[]; outputFirewall?: OutputFirewall | null; firewallOptions?: Record<string, unknown> }): Promise<Record<string, unknown>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class OutputFirewall {
|
|
72
|
+
constructor(options?: Record<string, unknown>);
|
|
73
|
+
inspect(output: unknown, options?: Record<string, unknown>): ReviewResult;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class ToolPermissionFirewall {
|
|
77
|
+
constructor(options?: Record<string, unknown>);
|
|
78
|
+
inspectCall(input: Record<string, unknown>): Record<string, unknown>;
|
|
79
|
+
inspectCallAsync?(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class RetrievalSanitizer {
|
|
83
|
+
constructor(options?: Record<string, unknown>);
|
|
84
|
+
sanitizeDocuments(documents: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class AuditTrail {
|
|
88
|
+
constructor(options?: Record<string, unknown>);
|
|
89
|
+
record(event?: Record<string, unknown>): Record<string, unknown>;
|
|
90
|
+
summarize(): Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const SHIELD_PRESETS: Record<string, Record<string, unknown>>;
|
|
94
|
+
export const CORE_INTERFACES: Record<string, string>;
|
|
95
|
+
export const POLICY_PACKS: Record<string, Record<string, unknown>>;
|
|
96
|
+
|
|
97
|
+
export function buildShieldOptions(options?: Record<string, unknown>): Record<string, unknown>;
|
|
98
|
+
export function summarizeOperationalTelemetry(events?: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
99
|
+
export function parseJsonOutput(output: unknown): unknown;
|
|
100
|
+
|
|
101
|
+
export function createOpenAIAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
102
|
+
export function createAnthropicAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
103
|
+
export function createGeminiAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
104
|
+
export function createOpenRouterAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { BlackwallShield, OutputFirewall, type ProviderAdapter } from './index';
|
|
2
|
+
|
|
3
|
+
export class BlackwallLangChainCallback {
|
|
4
|
+
constructor(options?: Record<string, unknown>);
|
|
5
|
+
handleLLMStart(llm: unknown, prompts?: string[]): Promise<unknown>;
|
|
6
|
+
guardMessages(messages: unknown, metadata?: Record<string, unknown>): Promise<unknown>;
|
|
7
|
+
handleLLMEnd(output: unknown): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class BlackwallLlamaIndexCallback {
|
|
11
|
+
constructor(options?: Record<string, unknown>);
|
|
12
|
+
onEventStart(event: unknown): Promise<unknown>;
|
|
13
|
+
onEventEnd(event: unknown): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createExpressMiddleware(options?: Record<string, unknown>): (req: unknown, res: unknown, next: () => void) => Promise<void>;
|
|
17
|
+
export function createLangChainCallbacks(options?: Record<string, unknown>): Record<string, unknown>;
|
|
18
|
+
export function createLlamaIndexCallback(options?: Record<string, unknown>): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpdeva/blackwall-llm-shield-js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Open-source JavaScript enterprise LLM protection toolkit for Node.js and Next.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Vish <hello@vish.au> (https://vish.au)",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"main": "src/index.js",
|
|
9
|
+
"types": "./index.d.ts",
|
|
9
10
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./integrations": {
|
|
16
|
+
"types": "./integrations.d.ts",
|
|
17
|
+
"default": "./src/integrations.js"
|
|
18
|
+
},
|
|
19
|
+
"./providers": {
|
|
20
|
+
"types": "./providers.d.ts",
|
|
21
|
+
"default": "./src/providers.js"
|
|
22
|
+
},
|
|
23
|
+
"./semantic": {
|
|
24
|
+
"types": "./semantic.d.ts",
|
|
25
|
+
"default": "./src/semantic.js"
|
|
26
|
+
}
|
|
14
27
|
},
|
|
15
28
|
"bin": {
|
|
16
29
|
"blackwall-scorecard": "src/scorecard.js"
|
|
@@ -26,6 +39,10 @@
|
|
|
26
39
|
},
|
|
27
40
|
"files": [
|
|
28
41
|
"src",
|
|
42
|
+
"index.d.ts",
|
|
43
|
+
"integrations.d.ts",
|
|
44
|
+
"providers.d.ts",
|
|
45
|
+
"semantic.d.ts",
|
|
29
46
|
"README.md",
|
|
30
47
|
"LICENSE",
|
|
31
48
|
"NOTICE"
|
package/providers.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ProviderAdapter } from './index';
|
|
2
|
+
|
|
3
|
+
export function createOpenAIAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
|
4
|
+
export function createAnthropicAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
|
5
|
+
export function createGeminiAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
|
6
|
+
export function createOpenRouterAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
package/semantic.d.ts
ADDED
package/src/index.js
CHANGED
|
@@ -145,6 +145,34 @@ const SHIELD_PRESETS = {
|
|
|
145
145
|
notifyOnRiskLevel: 'medium',
|
|
146
146
|
shadowMode: false,
|
|
147
147
|
},
|
|
148
|
+
agentPlanner: {
|
|
149
|
+
blockOnPromptInjection: true,
|
|
150
|
+
promptInjectionThreshold: 'medium',
|
|
151
|
+
notifyOnRiskLevel: 'medium',
|
|
152
|
+
shadowMode: true,
|
|
153
|
+
shadowPolicyPacks: ['government'],
|
|
154
|
+
},
|
|
155
|
+
documentReview: {
|
|
156
|
+
blockOnPromptInjection: true,
|
|
157
|
+
promptInjectionThreshold: 'high',
|
|
158
|
+
notifyOnRiskLevel: 'medium',
|
|
159
|
+
shadowMode: true,
|
|
160
|
+
policyPack: 'healthcare',
|
|
161
|
+
},
|
|
162
|
+
ragSearch: {
|
|
163
|
+
blockOnPromptInjection: true,
|
|
164
|
+
promptInjectionThreshold: 'medium',
|
|
165
|
+
notifyOnRiskLevel: 'medium',
|
|
166
|
+
shadowMode: true,
|
|
167
|
+
shadowPolicyPacks: ['government'],
|
|
168
|
+
},
|
|
169
|
+
toolCalling: {
|
|
170
|
+
blockOnPromptInjection: true,
|
|
171
|
+
promptInjectionThreshold: 'medium',
|
|
172
|
+
notifyOnRiskLevel: 'medium',
|
|
173
|
+
shadowMode: false,
|
|
174
|
+
policyPack: 'finance',
|
|
175
|
+
},
|
|
148
176
|
};
|
|
149
177
|
|
|
150
178
|
const CORE_INTERFACE_VERSION = '1.0';
|
|
@@ -152,6 +180,7 @@ const CORE_INTERFACES = Object.freeze({
|
|
|
152
180
|
guardModelRequest: CORE_INTERFACE_VERSION,
|
|
153
181
|
reviewModelResponse: CORE_INTERFACE_VERSION,
|
|
154
182
|
protectModelCall: CORE_INTERFACE_VERSION,
|
|
183
|
+
protectJsonModelCall: CORE_INTERFACE_VERSION,
|
|
155
184
|
toolPermissionFirewall: CORE_INTERFACE_VERSION,
|
|
156
185
|
retrievalSanitizer: CORE_INTERFACE_VERSION,
|
|
157
186
|
});
|
|
@@ -350,24 +379,64 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
350
379
|
shadowModeEvents: 0,
|
|
351
380
|
byType: {},
|
|
352
381
|
byRoute: {},
|
|
382
|
+
byFeature: {},
|
|
383
|
+
byTenant: {},
|
|
384
|
+
byModel: {},
|
|
385
|
+
byPolicyOutcome: {
|
|
386
|
+
blocked: 0,
|
|
387
|
+
shadowBlocked: 0,
|
|
388
|
+
allowed: 0,
|
|
389
|
+
},
|
|
390
|
+
topRules: {},
|
|
353
391
|
highestSeverity: 'low',
|
|
392
|
+
noisiestRoutes: [],
|
|
393
|
+
weeklyBlockEstimate: 0,
|
|
354
394
|
};
|
|
355
395
|
for (const event of Array.isArray(events) ? events : []) {
|
|
356
396
|
const type = event && event.type ? event.type : 'unknown';
|
|
357
|
-
const
|
|
397
|
+
const metadata = event && event.metadata ? event.metadata : {};
|
|
398
|
+
const route = metadata.route || metadata.path || 'unknown';
|
|
399
|
+
const feature = metadata.feature || metadata.capability || route;
|
|
400
|
+
const tenant = metadata.tenantId || metadata.tenant_id || 'unknown';
|
|
401
|
+
const model = metadata.model || metadata.modelName || 'unknown';
|
|
358
402
|
const severity = event && event.report && event.report.outputReview
|
|
359
403
|
? event.report.outputReview.severity
|
|
360
404
|
: (event && event.report && event.report.promptInjection ? event.report.promptInjection.level : 'low');
|
|
361
405
|
summary.totalEvents += 1;
|
|
362
406
|
summary.byType[type] = (summary.byType[type] || 0) + 1;
|
|
363
407
|
summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
|
|
408
|
+
summary.byFeature[feature] = (summary.byFeature[feature] || 0) + 1;
|
|
409
|
+
summary.byTenant[tenant] = (summary.byTenant[tenant] || 0) + 1;
|
|
410
|
+
summary.byModel[model] = (summary.byModel[model] || 0) + 1;
|
|
364
411
|
if (event && event.blocked) summary.blockedEvents += 1;
|
|
365
412
|
if (event && event.shadowMode) summary.shadowModeEvents += 1;
|
|
413
|
+
if (event && event.blocked) summary.byPolicyOutcome.blocked += 1;
|
|
414
|
+
else if (event && event.shadowMode) summary.byPolicyOutcome.shadowBlocked += 1;
|
|
415
|
+
else summary.byPolicyOutcome.allowed += 1;
|
|
416
|
+
const rules = event && event.report && event.report.promptInjection && Array.isArray(event.report.promptInjection.matches)
|
|
417
|
+
? event.report.promptInjection.matches.map((item) => item.id).filter(Boolean)
|
|
418
|
+
: [];
|
|
419
|
+
rules.forEach((rule) => {
|
|
420
|
+
summary.topRules[rule] = (summary.topRules[rule] || 0) + 1;
|
|
421
|
+
});
|
|
366
422
|
if (severityWeight(severity) > severityWeight(summary.highestSeverity)) summary.highestSeverity = severity;
|
|
367
423
|
}
|
|
424
|
+
summary.topRules = Object.fromEntries(
|
|
425
|
+
Object.entries(summary.topRules).sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
426
|
+
);
|
|
427
|
+
summary.noisiestRoutes = Object.entries(summary.byRoute)
|
|
428
|
+
.sort((a, b) => b[1] - a[1])
|
|
429
|
+
.slice(0, 5)
|
|
430
|
+
.map(([route, count]) => ({ route, count }));
|
|
431
|
+
summary.weeklyBlockEstimate = summary.byPolicyOutcome.blocked + summary.byPolicyOutcome.shadowBlocked;
|
|
368
432
|
return summary;
|
|
369
433
|
}
|
|
370
434
|
|
|
435
|
+
function parseJsonOutput(output) {
|
|
436
|
+
if (typeof output === 'string') return JSON.parse(output);
|
|
437
|
+
return output;
|
|
438
|
+
}
|
|
439
|
+
|
|
371
440
|
function resolveShieldPreset(name) {
|
|
372
441
|
if (!name) return {};
|
|
373
442
|
return SHIELD_PRESETS[name] ? { ...SHIELD_PRESETS[name] } : {};
|
|
@@ -1327,6 +1396,69 @@ class BlackwallShield {
|
|
|
1327
1396
|
},
|
|
1328
1397
|
});
|
|
1329
1398
|
}
|
|
1399
|
+
|
|
1400
|
+
async protectJsonModelCall({
|
|
1401
|
+
messages = [],
|
|
1402
|
+
metadata = {},
|
|
1403
|
+
allowSystemMessages = this.options.allowSystemMessages,
|
|
1404
|
+
comparePolicyPacks = [],
|
|
1405
|
+
callModel,
|
|
1406
|
+
mapMessages = null,
|
|
1407
|
+
mapOutput = null,
|
|
1408
|
+
outputFirewall = null,
|
|
1409
|
+
firewallOptions = {},
|
|
1410
|
+
requiredSchema = null,
|
|
1411
|
+
} = {}) {
|
|
1412
|
+
const result = await this.protectModelCall({
|
|
1413
|
+
messages,
|
|
1414
|
+
metadata,
|
|
1415
|
+
allowSystemMessages,
|
|
1416
|
+
comparePolicyPacks,
|
|
1417
|
+
callModel,
|
|
1418
|
+
mapMessages,
|
|
1419
|
+
mapOutput,
|
|
1420
|
+
outputFirewall,
|
|
1421
|
+
firewallOptions,
|
|
1422
|
+
});
|
|
1423
|
+
if (result.blocked) return result;
|
|
1424
|
+
try {
|
|
1425
|
+
const parsed = parseJsonOutput(result.review.maskedOutput != null ? result.review.maskedOutput : result.response);
|
|
1426
|
+
const schemaValid = validateRequiredSchema(parsed, requiredSchema);
|
|
1427
|
+
if (!schemaValid) {
|
|
1428
|
+
return {
|
|
1429
|
+
...result,
|
|
1430
|
+
allowed: false,
|
|
1431
|
+
blocked: true,
|
|
1432
|
+
stage: 'output',
|
|
1433
|
+
reason: 'Model output failed JSON schema validation',
|
|
1434
|
+
json: {
|
|
1435
|
+
parsed,
|
|
1436
|
+
schemaValid: false,
|
|
1437
|
+
},
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
...result,
|
|
1442
|
+
json: {
|
|
1443
|
+
parsed,
|
|
1444
|
+
schemaValid: true,
|
|
1445
|
+
},
|
|
1446
|
+
};
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
return {
|
|
1449
|
+
...result,
|
|
1450
|
+
allowed: false,
|
|
1451
|
+
blocked: true,
|
|
1452
|
+
stage: 'output',
|
|
1453
|
+
reason: 'Model output is not valid JSON',
|
|
1454
|
+
json: {
|
|
1455
|
+
parsed: null,
|
|
1456
|
+
schemaValid: false,
|
|
1457
|
+
parseError: error.message,
|
|
1458
|
+
},
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1330
1462
|
}
|
|
1331
1463
|
|
|
1332
1464
|
function validateGrounding(text, documents = [], options = {}) {
|
|
@@ -2035,6 +2167,7 @@ module.exports = {
|
|
|
2035
2167
|
runRedTeamSuite,
|
|
2036
2168
|
buildShieldOptions,
|
|
2037
2169
|
summarizeOperationalTelemetry,
|
|
2170
|
+
parseJsonOutput,
|
|
2038
2171
|
createOpenAIAdapter,
|
|
2039
2172
|
createAnthropicAdapter,
|
|
2040
2173
|
createGeminiAdapter,
|
package/src/providers.js
CHANGED
|
@@ -12,6 +12,38 @@ function stringifyContent(content) {
|
|
|
12
12
|
return String(content || '');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function toGeminiPart(item) {
|
|
16
|
+
if (typeof item === 'string') return { text: item };
|
|
17
|
+
if (!item || typeof item !== 'object') return null;
|
|
18
|
+
if ((item.type === 'text' || item.type === 'input_text') && typeof item.text === 'string') {
|
|
19
|
+
return { text: item.text };
|
|
20
|
+
}
|
|
21
|
+
if (item.type === 'image_url' && typeof item.image_url === 'string') {
|
|
22
|
+
return { fileData: { fileUri: item.image_url } };
|
|
23
|
+
}
|
|
24
|
+
if (item.type === 'file') {
|
|
25
|
+
if (item.file_data && typeof item.file_data === 'object') return { inlineData: item.file_data };
|
|
26
|
+
if (typeof item.file_uri === 'string') return { fileData: { fileUri: item.file_uri } };
|
|
27
|
+
if (typeof item.file_id === 'string') return { fileData: { fileUri: item.file_id } };
|
|
28
|
+
}
|
|
29
|
+
if (item.type === 'json' && typeof item.value === 'string') {
|
|
30
|
+
return { text: item.value };
|
|
31
|
+
}
|
|
32
|
+
if (typeof item.text === 'string') return { text: item.text };
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toGeminiParts(content) {
|
|
37
|
+
if (typeof content === 'string') return [{ text: content }];
|
|
38
|
+
if (Array.isArray(content)) return content.map((item) => toGeminiPart(item)).filter(Boolean);
|
|
39
|
+
if (content && typeof content === 'object') {
|
|
40
|
+
if (Array.isArray(content.parts)) return toGeminiParts(content.parts);
|
|
41
|
+
const part = toGeminiPart(content);
|
|
42
|
+
return part ? [part] : [{ text: stringifyContent(content) }];
|
|
43
|
+
}
|
|
44
|
+
return [{ text: String(content || '') }];
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
function toOpenAIInput(messages = []) {
|
|
16
48
|
return messages.map((message) => ({
|
|
17
49
|
role: message.role,
|
|
@@ -101,19 +133,30 @@ function createGeminiAdapter({ client, model, request = {}, extractOutput = null
|
|
|
101
133
|
return {
|
|
102
134
|
provider: 'gemini',
|
|
103
135
|
async invoke({ messages }) {
|
|
136
|
+
const systemInstruction = extractSystemPrompt(messages);
|
|
104
137
|
const response = await client.models.generateContent({
|
|
105
138
|
model,
|
|
106
|
-
contents: messages
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
contents: messages
|
|
140
|
+
.filter((message) => message.role !== 'system')
|
|
141
|
+
.map((message) => ({
|
|
142
|
+
role: message.role === 'assistant' ? 'model' : 'user',
|
|
143
|
+
parts: toGeminiParts(message.content),
|
|
144
|
+
})),
|
|
145
|
+
...(systemInstruction ? { systemInstruction: { parts: [{ text: systemInstruction }] } } : {}),
|
|
110
146
|
...request,
|
|
111
147
|
});
|
|
112
|
-
return defaultAdapterResult(response,
|
|
148
|
+
return defaultAdapterResult(response, this.extractOutput(response));
|
|
113
149
|
},
|
|
114
150
|
extractOutput(response) {
|
|
115
151
|
if (typeof extractOutput === 'function') return extractOutput(response);
|
|
116
152
|
if (response && typeof response.text === 'string') return response.text;
|
|
153
|
+
if (response && Array.isArray(response.candidates)) {
|
|
154
|
+
return response.candidates
|
|
155
|
+
.flatMap((candidate) => (((candidate || {}).content || {}).parts || []))
|
|
156
|
+
.map((part) => (part && typeof part.text === 'string' ? part.text : ''))
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join('\n');
|
|
159
|
+
}
|
|
117
160
|
if (typeof response === 'string') return response;
|
|
118
161
|
return '';
|
|
119
162
|
},
|