@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 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.1.7",
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(event);
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,