@vpdeva/blackwall-llm-shield-js 0.1.8 → 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 +58 -0
- package/index.d.ts +26 -0
- package/package.json +1 -1
- package/src/index.js +311 -2
package/README.md
CHANGED
|
@@ -89,6 +89,8 @@ The current recommendation for enterprise teams is a controlled pilot first: sta
|
|
|
89
89
|
|
|
90
90
|
Use `summarizeOperationalTelemetry()` with emitted telemetry events when you want route-level summaries, blocked-event counts, and rollout visibility for operators.
|
|
91
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
|
+
|
|
92
94
|
### Output grounding and tone review
|
|
93
95
|
|
|
94
96
|
`OutputFirewall` can compare responses against retrieved documents and flag hallucination-style unsupported claims or unprofessional tone.
|
|
@@ -119,6 +121,8 @@ Use it after the model responds to catch leaked secrets, dangerous code patterns
|
|
|
119
121
|
|
|
120
122
|
Use it to allowlist tools, block disallowed tools, validate arguments, and require approval for risky operations.
|
|
121
123
|
|
|
124
|
+
It can also integrate with `ValueAtRiskCircuitBreaker` for high-value actions and `ShadowConsensusAuditor` for secondary logic review before sensitive tools execute.
|
|
125
|
+
|
|
122
126
|
### `RetrievalSanitizer`
|
|
123
127
|
|
|
124
128
|
Use it before injecting retrieved documents into context so hostile instructions in your RAG data store do not quietly become model instructions.
|
|
@@ -142,6 +146,14 @@ Recommended presets:
|
|
|
142
146
|
|
|
143
147
|
Use it to record signed events, summarize security activity, and power dashboards or downstream analysis.
|
|
144
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
|
+
|
|
145
157
|
## Example Workflows
|
|
146
158
|
|
|
147
159
|
### Guard a request before calling the model
|
|
@@ -201,6 +213,48 @@ function createModelShield(shield) {
|
|
|
201
213
|
}
|
|
202
214
|
```
|
|
203
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
|
+
|
|
204
258
|
### Protect a strict JSON workflow
|
|
205
259
|
|
|
206
260
|
```js
|
|
@@ -319,6 +373,8 @@ const { summarizeOperationalTelemetry } = require('@vpdeva/blackwall-llm-shield-
|
|
|
319
373
|
const summary = summarizeOperationalTelemetry(events);
|
|
320
374
|
console.log(summary.byRoute);
|
|
321
375
|
console.log(summary.byFeature);
|
|
376
|
+
console.log(summary.byUser);
|
|
377
|
+
console.log(summary.byIdentityProvider);
|
|
322
378
|
console.log(summary.noisiestRoutes);
|
|
323
379
|
console.log(summary.weeklyBlockEstimate);
|
|
324
380
|
console.log(summary.highestSeverity);
|
|
@@ -372,6 +428,8 @@ For Gemini-heavy apps, the bundled adapter now preserves system instructions plu
|
|
|
372
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.
|
|
373
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.
|
|
374
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.
|
|
375
433
|
|
|
376
434
|
## Release Commands
|
|
377
435
|
|
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,
|