@vpdeva/blackwall-llm-shield-js 0.1.7 → 0.2.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 +103 -0
- package/index.d.ts +26 -0
- package/package.json +1 -1
- package/src/index.js +311 -2
package/README.md
CHANGED
|
@@ -81,10 +81,16 @@ Use `shadowMode` with `shadowPolicyPacks` or `comparePolicyPacks` to record what
|
|
|
81
81
|
|
|
82
82
|
Use `createOpenAIAdapter()`, `createAnthropicAdapter()`, `createGeminiAdapter()`, or `createOpenRouterAdapter()` with `protectWithAdapter()` when you want Blackwall to wrap the provider call end to end.
|
|
83
83
|
|
|
84
|
+
### Controlled-pilot rollout
|
|
85
|
+
|
|
86
|
+
The current recommendation for enterprise teams is a controlled pilot first: start in shadow mode, aggregate route-level telemetry, tune suppressions explicitly, then promote the cleanest routes to enforcement.
|
|
87
|
+
|
|
84
88
|
### Observability and control-plane support
|
|
85
89
|
|
|
86
90
|
Use `summarizeOperationalTelemetry()` with emitted telemetry events when you want route-level summaries, blocked-event counts, and rollout visibility for operators.
|
|
87
91
|
|
|
92
|
+
Enterprise deployments can also enrich emitted events with SSO/user context and forward flattened records to Power BI or other downstream reporting systems.
|
|
93
|
+
|
|
88
94
|
### Output grounding and tone review
|
|
89
95
|
|
|
90
96
|
`OutputFirewall` can compare responses against retrieved documents and flag hallucination-style unsupported claims or unprofessional tone.
|
|
@@ -115,6 +121,8 @@ Use it after the model responds to catch leaked secrets, dangerous code patterns
|
|
|
115
121
|
|
|
116
122
|
Use it to allowlist tools, block disallowed tools, validate arguments, and require approval for risky operations.
|
|
117
123
|
|
|
124
|
+
It can also integrate with `ValueAtRiskCircuitBreaker` for high-value actions and `ShadowConsensusAuditor` for secondary logic review before sensitive tools execute.
|
|
125
|
+
|
|
118
126
|
### `RetrievalSanitizer`
|
|
119
127
|
|
|
120
128
|
Use it before injecting retrieved documents into context so hostile instructions in your RAG data store do not quietly become model instructions.
|
|
@@ -138,6 +146,14 @@ Recommended presets:
|
|
|
138
146
|
|
|
139
147
|
Use it to record signed events, summarize security activity, and power dashboards or downstream analysis.
|
|
140
148
|
|
|
149
|
+
### Advanced Agent Controls
|
|
150
|
+
|
|
151
|
+
- `ValueAtRiskCircuitBreaker` for financial or high-value operational actions
|
|
152
|
+
- `ShadowConsensusAuditor` for second-model or secondary-review logic conflict checks
|
|
153
|
+
- `DigitalTwinOrchestrator` for mock tool environments and sandbox simulations
|
|
154
|
+
- `suggestPolicyOverride()` for narrow false-positive tuning suggestions after HITL approvals
|
|
155
|
+
- `AgentIdentityRegistry.issueSignedPassport()` for signed agent identity exchange
|
|
156
|
+
|
|
141
157
|
## Example Workflows
|
|
142
158
|
|
|
143
159
|
### Guard a request before calling the model
|
|
@@ -181,6 +197,64 @@ const result = await shield.protectWithAdapter({
|
|
|
181
197
|
console.log(result.stage, result.allowed);
|
|
182
198
|
```
|
|
183
199
|
|
|
200
|
+
### Wrap Blackwall behind your own app adapter
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
function createModelShield(shield) {
|
|
204
|
+
return {
|
|
205
|
+
async run({ messages, metadata, callProvider }) {
|
|
206
|
+
return shield.protectModelCall({
|
|
207
|
+
messages,
|
|
208
|
+
metadata,
|
|
209
|
+
callModel: callProvider,
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Add SSO-aware telemetry and Power BI export
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
const { BlackwallShield, PowerBIExporter } = require('@vpdeva/blackwall-llm-shield-js');
|
|
220
|
+
|
|
221
|
+
const shield = new BlackwallShield({
|
|
222
|
+
identityResolver: (metadata) => ({
|
|
223
|
+
userId: metadata.sso?.subject,
|
|
224
|
+
userEmail: metadata.sso?.email,
|
|
225
|
+
userName: metadata.sso?.displayName,
|
|
226
|
+
identityProvider: metadata.sso?.provider,
|
|
227
|
+
groups: metadata.sso?.groups,
|
|
228
|
+
}),
|
|
229
|
+
telemetryExporters: [
|
|
230
|
+
new PowerBIExporter({ endpointUrl: process.env.POWER_BI_PUSH_URL }),
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Protect high-value actions with a VaR breaker and consensus auditor
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
const firewall = new ToolPermissionFirewall({
|
|
239
|
+
allowedTools: ['issueRefund'],
|
|
240
|
+
valueAtRiskCircuitBreaker: new ValueAtRiskCircuitBreaker({ maxValuePerWindow: 5000 }),
|
|
241
|
+
consensusAuditor: new ShadowConsensusAuditor(),
|
|
242
|
+
consensusRequiredFor: ['issueRefund'],
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Generate a digital twin for sandbox testing
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
const twin = new DigitalTwinOrchestrator({
|
|
250
|
+
toolSchemas: [
|
|
251
|
+
{ name: 'lookupOrder', mockResponse: { orderId: 'ord_1', status: 'mocked' } },
|
|
252
|
+
],
|
|
253
|
+
}).generate();
|
|
254
|
+
|
|
255
|
+
await twin.simulateCall('lookupOrder', { orderId: 'ord_1' });
|
|
256
|
+
```
|
|
257
|
+
|
|
184
258
|
### Protect a strict JSON workflow
|
|
185
259
|
|
|
186
260
|
```js
|
|
@@ -218,6 +292,18 @@ const shield = new BlackwallShield({
|
|
|
218
292
|
});
|
|
219
293
|
```
|
|
220
294
|
|
|
295
|
+
### Next.js App Router plus Gemini pattern
|
|
296
|
+
|
|
297
|
+
For App Router route handlers, the cleanest production shape is:
|
|
298
|
+
|
|
299
|
+
- parse the request in `app/api/.../route.ts`
|
|
300
|
+
- use `preset: 'shadowFirst'` or a route-specific preset like `agentPlanner` or `documentReview`
|
|
301
|
+
- attach `route`, `feature`, and `tenantId` metadata
|
|
302
|
+
- wrap the Gemini SDK call with `createGeminiAdapter()` plus `protectWithAdapter()`
|
|
303
|
+
- ship `report.telemetry` and `onTelemetry` into a route-level log sink
|
|
304
|
+
|
|
305
|
+
That keeps request guarding, output review, and operator reporting in one path without scattering policy logic across the route.
|
|
306
|
+
|
|
221
307
|
### Route and domain examples
|
|
222
308
|
|
|
223
309
|
For RAG:
|
|
@@ -273,6 +359,13 @@ const shield = new BlackwallShield({
|
|
|
273
359
|
- Full provider wrapper: `protectWithAdapter()`
|
|
274
360
|
- Tool firewall + RAG sanitizer: `ToolPermissionFirewall` + `RetrievalSanitizer`
|
|
275
361
|
|
|
362
|
+
### False-positive tuning
|
|
363
|
+
|
|
364
|
+
- Start with route-level `shadowMode: true`
|
|
365
|
+
- Add `suppressPromptRules` only per route, not globally, so the reason for each suppression stays obvious
|
|
366
|
+
- Log `report.promptInjection.matches` and `report.telemetry.promptInjectionRuleHits` to explain why a request was flagged
|
|
367
|
+
- Review `summary.noisiestRoutes`, `summary.byFeature`, and `summary.weeklyBlockEstimate` before raising enforcement
|
|
368
|
+
|
|
276
369
|
### Operational telemetry summaries
|
|
277
370
|
|
|
278
371
|
```js
|
|
@@ -280,6 +373,8 @@ const { summarizeOperationalTelemetry } = require('@vpdeva/blackwall-llm-shield-
|
|
|
280
373
|
const summary = summarizeOperationalTelemetry(events);
|
|
281
374
|
console.log(summary.byRoute);
|
|
282
375
|
console.log(summary.byFeature);
|
|
376
|
+
console.log(summary.byUser);
|
|
377
|
+
console.log(summary.byIdentityProvider);
|
|
283
378
|
console.log(summary.noisiestRoutes);
|
|
284
379
|
console.log(summary.weeklyBlockEstimate);
|
|
285
380
|
console.log(summary.highestSeverity);
|
|
@@ -328,6 +423,14 @@ For Next.js, the most production-real patterns are App Router route handlers, se
|
|
|
328
423
|
|
|
329
424
|
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
425
|
|
|
426
|
+
## Enterprise Adoption Notes
|
|
427
|
+
|
|
428
|
+
- A controlled pilot is a good fit today when you want shadow-mode prompt and output protection without forcing hard blocking on every route immediately.
|
|
429
|
+
- If you prefer not to depend on Blackwall directly everywhere, wrap it behind your own internal model-security abstraction and expose only the contract your app teams need.
|
|
430
|
+
- For broader approval, focus rollout reviews on false-positive rates, noisiest routes, and latency budgets alongside jailbreak coverage.
|
|
431
|
+
- For executive or staff-facing workflows, always attach authenticated identity metadata so telemetry can answer which user triggered which risky request or output event.
|
|
432
|
+
- For high-impact agentic workflows, combine tool approval, VaR limits, digital-twin tests, and signed agent passports instead of relying on a single detector.
|
|
433
|
+
|
|
331
434
|
## Release Commands
|
|
332
435
|
|
|
333
436
|
- `npm run release:check` runs the JS test suite before release
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface ShieldOptions {
|
|
|
55
55
|
routePolicies?: Array<{ route: string | RegExp | ((route: string, metadata: Record<string, unknown>) => boolean); options: Record<string, unknown> }>;
|
|
56
56
|
customPromptDetectors?: Array<(payload: Record<string, unknown>) => Record<string, unknown> | Array<Record<string, unknown>> | null>;
|
|
57
57
|
onTelemetry?: (event: Record<string, unknown>) => void | Promise<void>;
|
|
58
|
+
telemetryExporters?: Array<{ send(events: Array<Record<string, unknown>>): unknown }>;
|
|
59
|
+
identityResolver?: (metadata: Record<string, unknown>) => Record<string, unknown> | null;
|
|
58
60
|
[key: string]: unknown;
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -79,6 +81,22 @@ export class ToolPermissionFirewall {
|
|
|
79
81
|
inspectCallAsync?(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
export class ValueAtRiskCircuitBreaker {
|
|
85
|
+
constructor(options?: Record<string, unknown>);
|
|
86
|
+
inspect(input?: Record<string, unknown>): Record<string, unknown>;
|
|
87
|
+
revokeSession(sessionId: string, durationMs?: number): Record<string, unknown> | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class ShadowConsensusAuditor {
|
|
91
|
+
constructor(options?: Record<string, unknown>);
|
|
92
|
+
inspect(input?: Record<string, unknown>): Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class DigitalTwinOrchestrator {
|
|
96
|
+
constructor(options?: Record<string, unknown>);
|
|
97
|
+
generate(): Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
export class RetrievalSanitizer {
|
|
83
101
|
constructor(options?: Record<string, unknown>);
|
|
84
102
|
sanitizeDocuments(documents: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
|
|
@@ -97,6 +115,14 @@ export const POLICY_PACKS: Record<string, Record<string, unknown>>;
|
|
|
97
115
|
export function buildShieldOptions(options?: Record<string, unknown>): Record<string, unknown>;
|
|
98
116
|
export function summarizeOperationalTelemetry(events?: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
99
117
|
export function parseJsonOutput(output: unknown): unknown;
|
|
118
|
+
export function normalizeIdentityMetadata(metadata?: Record<string, unknown>, resolver?: ((metadata: Record<string, unknown>) => Record<string, unknown> | null) | null): Record<string, unknown>;
|
|
119
|
+
export function buildEnterpriseTelemetryEvent(event?: Record<string, unknown>, resolver?: ((metadata: Record<string, unknown>) => Record<string, unknown> | null) | null): Record<string, unknown>;
|
|
120
|
+
export function buildPowerBIRecord(event?: Record<string, unknown>): Record<string, unknown>;
|
|
121
|
+
export function suggestPolicyOverride(input?: Record<string, unknown>): Record<string, unknown> | null;
|
|
122
|
+
export class PowerBIExporter {
|
|
123
|
+
constructor(options?: Record<string, unknown>);
|
|
124
|
+
send(events?: Array<Record<string, unknown>> | Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
|
|
125
|
+
}
|
|
100
126
|
|
|
101
127
|
export function createOpenAIAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
102
128
|
export function createAnthropicAdapter(input: Record<string, unknown>): ProviderAdapter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpdeva/blackwall-llm-shield-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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)",
|
package/src/index.js
CHANGED
|
@@ -372,6 +372,83 @@ function createTelemetryEvent(type, payload = {}) {
|
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
function normalizeIdentityMetadata(metadata = {}, resolver = null) {
|
|
376
|
+
const resolved = typeof resolver === 'function' ? resolver(metadata) || {} : {};
|
|
377
|
+
const source = { ...metadata, ...resolved };
|
|
378
|
+
const groups = Array.isArray(source.groups)
|
|
379
|
+
? source.groups
|
|
380
|
+
: (Array.isArray(source.ssoGroups) ? source.ssoGroups : (typeof source.groups === 'string' ? source.groups.split(',').map((item) => item.trim()).filter(Boolean) : []));
|
|
381
|
+
return {
|
|
382
|
+
userId: source.userId || source.user_id || source.subject || source.sub || 'anonymous',
|
|
383
|
+
userEmail: source.userEmail || source.user_email || source.email || source.upn || null,
|
|
384
|
+
userName: source.userName || source.user_name || source.displayName || source.display_name || source.name || null,
|
|
385
|
+
tenantId: source.tenantId || source.tenant_id || source.orgId || source.org_id || 'default',
|
|
386
|
+
identityProvider: source.identityProvider || source.identity_provider || source.ssoProvider || source.sso_provider || source.idp || null,
|
|
387
|
+
authMethod: source.authMethod || source.auth_method || source.authType || source.auth_type || null,
|
|
388
|
+
sessionId: source.sessionId || source.session_id || null,
|
|
389
|
+
groups,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildEnterpriseTelemetryEvent(event = {}, resolver = null) {
|
|
394
|
+
const metadata = event && event.metadata ? event.metadata : {};
|
|
395
|
+
const actor = normalizeIdentityMetadata(metadata, resolver);
|
|
396
|
+
return {
|
|
397
|
+
...event,
|
|
398
|
+
actor,
|
|
399
|
+
metadata: {
|
|
400
|
+
...metadata,
|
|
401
|
+
userId: metadata.userId || metadata.user_id || actor.userId,
|
|
402
|
+
tenantId: metadata.tenantId || metadata.tenant_id || actor.tenantId,
|
|
403
|
+
identityProvider: metadata.identityProvider || metadata.identity_provider || actor.identityProvider,
|
|
404
|
+
sessionId: metadata.sessionId || metadata.session_id || actor.sessionId,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildPowerBIRecord(event = {}) {
|
|
410
|
+
const actor = event.actor || normalizeIdentityMetadata(event.metadata || {});
|
|
411
|
+
const metadata = event.metadata || {};
|
|
412
|
+
return {
|
|
413
|
+
eventType: event.type || 'unknown',
|
|
414
|
+
createdAt: event.createdAt || new Date().toISOString(),
|
|
415
|
+
route: metadata.route || metadata.path || 'unknown',
|
|
416
|
+
feature: metadata.feature || metadata.capability || metadata.route || 'unknown',
|
|
417
|
+
model: metadata.model || metadata.modelName || 'unknown',
|
|
418
|
+
tenantId: actor.tenantId || 'default',
|
|
419
|
+
userId: actor.userId || 'anonymous',
|
|
420
|
+
userEmail: actor.userEmail || null,
|
|
421
|
+
userName: actor.userName || null,
|
|
422
|
+
identityProvider: actor.identityProvider || null,
|
|
423
|
+
authMethod: actor.authMethod || null,
|
|
424
|
+
sessionId: actor.sessionId || null,
|
|
425
|
+
blocked: !!event.blocked,
|
|
426
|
+
shadowMode: !!event.shadowMode,
|
|
427
|
+
severity: (event.report && event.report.outputReview && event.report.outputReview.severity)
|
|
428
|
+
|| (event.report && event.report.promptInjection && event.report.promptInjection.level)
|
|
429
|
+
|| 'low',
|
|
430
|
+
topRule: (event.report && event.report.promptInjection && Array.isArray(event.report.promptInjection.matches) && event.report.promptInjection.matches[0] && event.report.promptInjection.matches[0].id) || null,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
class PowerBIExporter {
|
|
435
|
+
constructor(options = {}) {
|
|
436
|
+
this.endpointUrl = options.endpointUrl || options.webhookUrl || null;
|
|
437
|
+
this.fetchImpl = options.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async send(events = []) {
|
|
441
|
+
const records = (Array.isArray(events) ? events : [events]).filter(Boolean).map((event) => buildPowerBIRecord(event));
|
|
442
|
+
if (!this.endpointUrl || !this.fetchImpl) return records;
|
|
443
|
+
await this.fetchImpl(this.endpointUrl, {
|
|
444
|
+
method: 'POST',
|
|
445
|
+
headers: { 'Content-Type': 'application/json' },
|
|
446
|
+
body: JSON.stringify(records),
|
|
447
|
+
});
|
|
448
|
+
return records;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
375
452
|
function summarizeOperationalTelemetry(events = []) {
|
|
376
453
|
const summary = {
|
|
377
454
|
totalEvents: 0,
|
|
@@ -380,6 +457,8 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
380
457
|
byType: {},
|
|
381
458
|
byRoute: {},
|
|
382
459
|
byFeature: {},
|
|
460
|
+
byUser: {},
|
|
461
|
+
byIdentityProvider: {},
|
|
383
462
|
byTenant: {},
|
|
384
463
|
byModel: {},
|
|
385
464
|
byPolicyOutcome: {
|
|
@@ -398,6 +477,8 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
398
477
|
const route = metadata.route || metadata.path || 'unknown';
|
|
399
478
|
const feature = metadata.feature || metadata.capability || route;
|
|
400
479
|
const tenant = metadata.tenantId || metadata.tenant_id || 'unknown';
|
|
480
|
+
const user = metadata.userId || metadata.user_id || (event.actor && event.actor.userId) || 'unknown';
|
|
481
|
+
const idp = metadata.identityProvider || metadata.identity_provider || (event.actor && event.actor.identityProvider) || 'unknown';
|
|
401
482
|
const model = metadata.model || metadata.modelName || 'unknown';
|
|
402
483
|
const severity = event && event.report && event.report.outputReview
|
|
403
484
|
? event.report.outputReview.severity
|
|
@@ -406,6 +487,8 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
406
487
|
summary.byType[type] = (summary.byType[type] || 0) + 1;
|
|
407
488
|
summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
|
|
408
489
|
summary.byFeature[feature] = (summary.byFeature[feature] || 0) + 1;
|
|
490
|
+
summary.byUser[user] = (summary.byUser[user] || 0) + 1;
|
|
491
|
+
summary.byIdentityProvider[idp] = (summary.byIdentityProvider[idp] || 0) + 1;
|
|
409
492
|
summary.byTenant[tenant] = (summary.byTenant[tenant] || 0) + 1;
|
|
410
493
|
summary.byModel[model] = (summary.byModel[model] || 0) + 1;
|
|
411
494
|
if (event && event.blocked) summary.blockedEvents += 1;
|
|
@@ -1112,6 +1195,8 @@ class BlackwallShield {
|
|
|
1112
1195
|
outputFirewallDefaults: {},
|
|
1113
1196
|
onAlert: null,
|
|
1114
1197
|
onTelemetry: null,
|
|
1198
|
+
telemetryExporters: [],
|
|
1199
|
+
identityResolver: null,
|
|
1115
1200
|
webhookUrl: null,
|
|
1116
1201
|
...options,
|
|
1117
1202
|
};
|
|
@@ -1143,8 +1228,15 @@ class BlackwallShield {
|
|
|
1143
1228
|
}
|
|
1144
1229
|
|
|
1145
1230
|
async emitTelemetry(event) {
|
|
1231
|
+
const enriched = buildEnterpriseTelemetryEvent(event, this.options.identityResolver);
|
|
1146
1232
|
if (typeof this.options.onTelemetry === 'function') {
|
|
1147
|
-
await this.options.onTelemetry(
|
|
1233
|
+
await this.options.onTelemetry(enriched);
|
|
1234
|
+
}
|
|
1235
|
+
const exporters = Array.isArray(this.options.telemetryExporters) ? this.options.telemetryExporters : [];
|
|
1236
|
+
for (const exporter of exporters) {
|
|
1237
|
+
if (exporter && typeof exporter.send === 'function') {
|
|
1238
|
+
await exporter.send([enriched]);
|
|
1239
|
+
}
|
|
1148
1240
|
}
|
|
1149
1241
|
}
|
|
1150
1242
|
|
|
@@ -1563,9 +1655,10 @@ class CoTScanner {
|
|
|
1563
1655
|
}
|
|
1564
1656
|
|
|
1565
1657
|
class AgentIdentityRegistry {
|
|
1566
|
-
constructor() {
|
|
1658
|
+
constructor(options = {}) {
|
|
1567
1659
|
this.identities = new Map();
|
|
1568
1660
|
this.ephemeralTokens = new Map();
|
|
1661
|
+
this.secret = options.secret || 'blackwall-agent-passport-secret';
|
|
1569
1662
|
}
|
|
1570
1663
|
|
|
1571
1664
|
register(agentId, profile = {}) {
|
|
@@ -1595,6 +1688,177 @@ class AgentIdentityRegistry {
|
|
|
1595
1688
|
}
|
|
1596
1689
|
return { valid: true, agentId: record.agentId };
|
|
1597
1690
|
}
|
|
1691
|
+
|
|
1692
|
+
issueSignedPassport(agentId, options = {}) {
|
|
1693
|
+
const identity = this.get(agentId) || this.register(agentId, options.profile || {});
|
|
1694
|
+
const securityScore = options.securityScore != null
|
|
1695
|
+
? options.securityScore
|
|
1696
|
+
: Math.max(0, 100 - (Object.values(identity.capabilities || {}).filter(Boolean).length * 10));
|
|
1697
|
+
const passport = {
|
|
1698
|
+
agentId,
|
|
1699
|
+
issuedAt: new Date().toISOString(),
|
|
1700
|
+
issuer: options.issuer || 'blackwall-llm-shield-js',
|
|
1701
|
+
blackwallProtected: options.blackwallProtected !== false,
|
|
1702
|
+
securityScore,
|
|
1703
|
+
scopes: identity.scopes || [],
|
|
1704
|
+
persona: identity.persona || 'default',
|
|
1705
|
+
environment: options.environment || 'production',
|
|
1706
|
+
};
|
|
1707
|
+
const signature = crypto.createHmac('sha256', this.secret).update(JSON.stringify(passport)).digest('hex');
|
|
1708
|
+
return { ...passport, signature };
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
verifySignedPassport(passport = {}) {
|
|
1712
|
+
const { signature, ...unsigned } = passport || {};
|
|
1713
|
+
if (!signature) return { valid: false, reason: 'Passport signature is required' };
|
|
1714
|
+
const expected = crypto.createHmac('sha256', this.secret).update(JSON.stringify(unsigned)).digest('hex');
|
|
1715
|
+
return {
|
|
1716
|
+
valid: crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)),
|
|
1717
|
+
agentId: unsigned.agentId || null,
|
|
1718
|
+
securityScore: unsigned.securityScore || null,
|
|
1719
|
+
blackwallProtected: !!unsigned.blackwallProtected,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
class ValueAtRiskCircuitBreaker {
|
|
1725
|
+
constructor(options = {}) {
|
|
1726
|
+
this.maxValuePerWindow = options.maxValuePerWindow || 5000;
|
|
1727
|
+
this.windowMs = options.windowMs || (60 * 60 * 1000);
|
|
1728
|
+
this.revocationMs = options.revocationMs || (30 * 60 * 1000);
|
|
1729
|
+
this.valueExtractor = options.valueExtractor || ((args = {}, context = {}) => Number(context.actionValue != null ? context.actionValue : (args.amount != null ? args.amount : 0)));
|
|
1730
|
+
this.entries = [];
|
|
1731
|
+
this.revocations = new Map();
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
revokeSession(sessionId, durationMs = this.revocationMs) {
|
|
1735
|
+
if (!sessionId) return null;
|
|
1736
|
+
const expiresAt = Date.now() + durationMs;
|
|
1737
|
+
this.revocations.set(sessionId, expiresAt);
|
|
1738
|
+
return { sessionId, revokedUntil: new Date(expiresAt).toISOString() };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
inspect({ tool, args = {}, context = {} } = {}) {
|
|
1742
|
+
const sessionId = context.sessionId || context.session_id || null;
|
|
1743
|
+
const now = Date.now();
|
|
1744
|
+
const revokedUntil = sessionId ? this.revocations.get(sessionId) : null;
|
|
1745
|
+
if (revokedUntil && revokedUntil > now) {
|
|
1746
|
+
return {
|
|
1747
|
+
allowed: false,
|
|
1748
|
+
triggered: true,
|
|
1749
|
+
requiresMfa: true,
|
|
1750
|
+
reason: 'Session is revoked until MFA or human review completes',
|
|
1751
|
+
revokedSession: sessionId,
|
|
1752
|
+
revokedUntil: new Date(revokedUntil).toISOString(),
|
|
1753
|
+
riskWindowValue: null,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
this.entries = this.entries.filter((entry) => (now - entry.at) <= this.windowMs);
|
|
1757
|
+
const actionValue = Math.max(0, Number(this.valueExtractor(args, context) || 0));
|
|
1758
|
+
const key = sessionId || context.agentId || context.agent_id || context.userId || context.user_id || 'default';
|
|
1759
|
+
const relevant = this.entries.filter((entry) => entry.key === key);
|
|
1760
|
+
const riskWindowValue = relevant.reduce((sum, entry) => sum + entry.value, 0) + actionValue;
|
|
1761
|
+
const triggered = riskWindowValue > this.maxValuePerWindow;
|
|
1762
|
+
if (triggered) {
|
|
1763
|
+
const revocation = this.revokeSession(sessionId);
|
|
1764
|
+
return {
|
|
1765
|
+
allowed: false,
|
|
1766
|
+
triggered: true,
|
|
1767
|
+
requiresMfa: true,
|
|
1768
|
+
reason: `Value-at-risk threshold exceeded for ${tool || 'action'}`,
|
|
1769
|
+
riskWindowValue,
|
|
1770
|
+
threshold: this.maxValuePerWindow,
|
|
1771
|
+
actionValue,
|
|
1772
|
+
revokedSession: revocation && revocation.sessionId,
|
|
1773
|
+
revokedUntil: revocation && revocation.revokedUntil,
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
this.entries.push({ key, tool: tool || 'unknown', value: actionValue, at: now });
|
|
1777
|
+
return {
|
|
1778
|
+
allowed: true,
|
|
1779
|
+
triggered: false,
|
|
1780
|
+
requiresMfa: false,
|
|
1781
|
+
riskWindowValue,
|
|
1782
|
+
threshold: this.maxValuePerWindow,
|
|
1783
|
+
actionValue,
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
class ShadowConsensusAuditor {
|
|
1789
|
+
constructor(options = {}) {
|
|
1790
|
+
this.review = options.review || ((payload = {}) => {
|
|
1791
|
+
const text = JSON.stringify({ tool: payload.tool, args: payload.args, sessionContext: payload.sessionContext || '' }).toLowerCase();
|
|
1792
|
+
const disagreement = /\b(ignore previous|bypass|override|secret|reveal)\b/i.test(text);
|
|
1793
|
+
return {
|
|
1794
|
+
agreed: !disagreement,
|
|
1795
|
+
disagreement,
|
|
1796
|
+
reason: disagreement ? 'Logic Conflict: shadow auditor found risky reasoning drift' : null,
|
|
1797
|
+
};
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
inspect(payload = {}) {
|
|
1802
|
+
const result = this.review(payload) || {};
|
|
1803
|
+
return {
|
|
1804
|
+
agreed: result.agreed !== false,
|
|
1805
|
+
disagreement: !!result.disagreement || result.agreed === false,
|
|
1806
|
+
reason: result.reason || (result.agreed === false ? 'Logic Conflict detected by shadow auditor' : null),
|
|
1807
|
+
auditor: result.auditor || 'shadow',
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
class DigitalTwinOrchestrator {
|
|
1813
|
+
constructor(options = {}) {
|
|
1814
|
+
this.toolSchemas = options.toolSchemas || [];
|
|
1815
|
+
this.invocations = [];
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
generate() {
|
|
1819
|
+
const handlers = {};
|
|
1820
|
+
for (const schema of this.toolSchemas) {
|
|
1821
|
+
if (!schema || !schema.name) continue;
|
|
1822
|
+
handlers[schema.name] = async (args = {}) => {
|
|
1823
|
+
const response = schema.mockResponse || schema.sampleResponse || { ok: true, tool: schema.name, args };
|
|
1824
|
+
this.invocations.push({ tool: schema.name, args, response, at: new Date().toISOString() });
|
|
1825
|
+
return response;
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
return {
|
|
1829
|
+
handlers,
|
|
1830
|
+
simulateCall: async (tool, args = {}) => {
|
|
1831
|
+
if (!handlers[tool]) throw new Error(`No digital twin registered for ${tool}`);
|
|
1832
|
+
return handlers[tool](args);
|
|
1833
|
+
},
|
|
1834
|
+
invocations: this.invocations,
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function suggestPolicyOverride({ route = null, approval = null, guardResult = null, toolDecision = null } = {}) {
|
|
1840
|
+
if (approval !== true) return null;
|
|
1841
|
+
if (guardResult && guardResult.report && guardResult.report.promptInjection) {
|
|
1842
|
+
const rules = (guardResult.report.promptInjection.matches || []).map((item) => item.id).filter(Boolean);
|
|
1843
|
+
return {
|
|
1844
|
+
route: route || (guardResult.report.metadata && (guardResult.report.metadata.route || guardResult.report.metadata.path)) || '*',
|
|
1845
|
+
options: {
|
|
1846
|
+
shadowMode: true,
|
|
1847
|
+
suppressPromptRules: [...new Set(rules)],
|
|
1848
|
+
},
|
|
1849
|
+
rationale: 'Suggested from approved false positive',
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (toolDecision && toolDecision.approvalRequest) {
|
|
1853
|
+
return {
|
|
1854
|
+
route: route || ((toolDecision.approvalRequest.context || {}).route) || '*',
|
|
1855
|
+
options: {
|
|
1856
|
+
requireHumanApprovalFor: [toolDecision.approvalRequest.tool],
|
|
1857
|
+
},
|
|
1858
|
+
rationale: 'Suggested from approved high-impact tool action',
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
return null;
|
|
1598
1862
|
}
|
|
1599
1863
|
|
|
1600
1864
|
class AgenticCapabilityGater {
|
|
@@ -1737,6 +2001,9 @@ class ToolPermissionFirewall {
|
|
|
1737
2001
|
validators: {},
|
|
1738
2002
|
requireHumanApprovalFor: [],
|
|
1739
2003
|
capabilityGater: null,
|
|
2004
|
+
valueAtRiskCircuitBreaker: null,
|
|
2005
|
+
consensusAuditor: null,
|
|
2006
|
+
consensusRequiredFor: [],
|
|
1740
2007
|
onApprovalRequest: null,
|
|
1741
2008
|
approvalWebhookUrl: null,
|
|
1742
2009
|
...options,
|
|
@@ -1766,6 +2033,37 @@ class ToolPermissionFirewall {
|
|
|
1766
2033
|
return { allowed: false, reason: gate.reason, requiresApproval: false, agentGate: gate };
|
|
1767
2034
|
}
|
|
1768
2035
|
}
|
|
2036
|
+
if (this.options.valueAtRiskCircuitBreaker) {
|
|
2037
|
+
const breaker = this.options.valueAtRiskCircuitBreaker.inspect({ tool, args, context });
|
|
2038
|
+
if (!breaker.allowed) {
|
|
2039
|
+
return {
|
|
2040
|
+
allowed: false,
|
|
2041
|
+
reason: breaker.reason,
|
|
2042
|
+
requiresApproval: true,
|
|
2043
|
+
requiresMfa: !!breaker.requiresMfa,
|
|
2044
|
+
circuitBreaker: breaker,
|
|
2045
|
+
approvalRequest: { tool, args, context, breaker },
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (this.options.consensusAuditor && (context.highImpact || this.options.consensusRequiredFor.includes(tool))) {
|
|
2050
|
+
const consensus = this.options.consensusAuditor.inspect({
|
|
2051
|
+
tool,
|
|
2052
|
+
args,
|
|
2053
|
+
context,
|
|
2054
|
+
sessionContext: context.sessionContext || context.session_buffer || null,
|
|
2055
|
+
});
|
|
2056
|
+
if (consensus.disagreement) {
|
|
2057
|
+
return {
|
|
2058
|
+
allowed: false,
|
|
2059
|
+
reason: consensus.reason || 'Logic Conflict detected by shadow auditor',
|
|
2060
|
+
requiresApproval: true,
|
|
2061
|
+
logicConflict: true,
|
|
2062
|
+
consensus,
|
|
2063
|
+
approvalRequest: { tool, args, context, consensus },
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1769
2067
|
const requiresApproval = this.options.requireHumanApprovalFor.includes(tool);
|
|
1770
2068
|
return {
|
|
1771
2069
|
allowed: !requiresApproval,
|
|
@@ -1860,11 +2158,14 @@ class AuditTrail {
|
|
|
1860
2158
|
constructor(options = {}) {
|
|
1861
2159
|
this.secret = options.secret || 'blackwall-default-secret';
|
|
1862
2160
|
this.events = [];
|
|
2161
|
+
this.identityResolver = options.identityResolver || null;
|
|
1863
2162
|
}
|
|
1864
2163
|
|
|
1865
2164
|
record(event = {}) {
|
|
2165
|
+
const actor = event.actor || normalizeIdentityMetadata(event.metadata || event, this.identityResolver);
|
|
1866
2166
|
const payload = {
|
|
1867
2167
|
...event,
|
|
2168
|
+
actor,
|
|
1868
2169
|
complianceMap: event.complianceMap || mapCompliance([
|
|
1869
2170
|
...(event.ruleIds || []),
|
|
1870
2171
|
event.type === 'retrieval_poisoning_detected' ? 'retrieval_poisoning' : null,
|
|
@@ -2130,14 +2431,18 @@ module.exports = {
|
|
|
2130
2431
|
AuditTrail,
|
|
2131
2432
|
BlackwallShield,
|
|
2132
2433
|
CoTScanner,
|
|
2434
|
+
DigitalTwinOrchestrator,
|
|
2133
2435
|
ImageMetadataScanner,
|
|
2134
2436
|
LightweightIntentScorer,
|
|
2135
2437
|
MCPSecurityProxy,
|
|
2136
2438
|
OutputFirewall,
|
|
2439
|
+
PowerBIExporter,
|
|
2137
2440
|
RetrievalSanitizer,
|
|
2138
2441
|
SessionBuffer,
|
|
2442
|
+
ShadowConsensusAuditor,
|
|
2139
2443
|
TokenBudgetFirewall,
|
|
2140
2444
|
ToolPermissionFirewall,
|
|
2445
|
+
ValueAtRiskCircuitBreaker,
|
|
2141
2446
|
VisualInstructionDetector,
|
|
2142
2447
|
SENSITIVE_PATTERNS,
|
|
2143
2448
|
PROMPT_INJECTION_RULES,
|
|
@@ -2167,6 +2472,10 @@ module.exports = {
|
|
|
2167
2472
|
runRedTeamSuite,
|
|
2168
2473
|
buildShieldOptions,
|
|
2169
2474
|
summarizeOperationalTelemetry,
|
|
2475
|
+
suggestPolicyOverride,
|
|
2476
|
+
normalizeIdentityMetadata,
|
|
2477
|
+
buildEnterpriseTelemetryEvent,
|
|
2478
|
+
buildPowerBIRecord,
|
|
2170
2479
|
parseJsonOutput,
|
|
2171
2480
|
createOpenAIAdapter,
|
|
2172
2481
|
createAnthropicAdapter,
|