@vpdeva/blackwall-llm-shield-js 0.1.8 → 0.2.3
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 +94 -2
- package/index.d.ts +73 -0
- package/package.json +4 -1
- package/src/index.js +632 -2
package/README.md
CHANGED
|
@@ -26,9 +26,10 @@ JavaScript security middleware for LLM applications in Node.js and Next.js. Blac
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
npm install @vpdeva/blackwall-llm-shield-js
|
|
29
|
-
npm install @xenova/transformers
|
|
30
29
|
```
|
|
31
30
|
|
|
31
|
+
The package now ships with local Transformers support wired as a first-class dependency, so teams do not need a second install step just to enable semantic scoring.
|
|
32
|
+
|
|
32
33
|
## Fast Start
|
|
33
34
|
|
|
34
35
|
```js
|
|
@@ -89,6 +90,8 @@ The current recommendation for enterprise teams is a controlled pilot first: sta
|
|
|
89
90
|
|
|
90
91
|
Use `summarizeOperationalTelemetry()` with emitted telemetry events when you want route-level summaries, blocked-event counts, and rollout visibility for operators.
|
|
91
92
|
|
|
93
|
+
Enterprise deployments can also enrich emitted events with SSO/user context and forward flattened records to Power BI or other downstream reporting systems.
|
|
94
|
+
|
|
92
95
|
### Output grounding and tone review
|
|
93
96
|
|
|
94
97
|
`OutputFirewall` can compare responses against retrieved documents and flag hallucination-style unsupported claims or unprofessional tone.
|
|
@@ -119,13 +122,15 @@ Use it after the model responds to catch leaked secrets, dangerous code patterns
|
|
|
119
122
|
|
|
120
123
|
Use it to allowlist tools, block disallowed tools, validate arguments, and require approval for risky operations.
|
|
121
124
|
|
|
125
|
+
It can also integrate with `ValueAtRiskCircuitBreaker` for high-value actions and `ShadowConsensusAuditor` for secondary logic review before sensitive tools execute.
|
|
126
|
+
|
|
122
127
|
### `RetrievalSanitizer`
|
|
123
128
|
|
|
124
129
|
Use it before injecting retrieved documents into context so hostile instructions in your RAG data store do not quietly become model instructions.
|
|
125
130
|
|
|
126
131
|
### Contract Stability
|
|
127
132
|
|
|
128
|
-
The 0.
|
|
133
|
+
The 0.2.x line treats `guardModelRequest()`, `protectWithAdapter()`, `reviewModelResponse()`, `ToolPermissionFirewall`, and `RetrievalSanitizer` as the long-term integration contracts. The exported `CORE_INTERFACES` map can be logged or asserted by applications that want to pin expected behavior.
|
|
129
134
|
|
|
130
135
|
Recommended presets:
|
|
131
136
|
|
|
@@ -137,11 +142,36 @@ Recommended presets:
|
|
|
137
142
|
- `documentReview` for classification and document-review pipelines
|
|
138
143
|
- `ragSearch` for search-heavy retrieval endpoints
|
|
139
144
|
- `toolCalling` for routes that broker external actions
|
|
145
|
+
- `governmentStrict` for highly regulated public-sector and records-sensitive workflows
|
|
146
|
+
- `bankingPayments` for high-value payment and financial action routes
|
|
147
|
+
- `documentIntake` for upload-heavy intake and review flows
|
|
148
|
+
- `citizenServices` for identity-aware service delivery workflows
|
|
149
|
+
- `internalOpsAgent` for internal operational assistants with shadow-first defaults
|
|
150
|
+
|
|
151
|
+
### Global Governance Pack
|
|
152
|
+
|
|
153
|
+
The 0.2.2 line also adds globally applicable enterprise controls that are useful across regulated industries, not just one country or sector:
|
|
154
|
+
|
|
155
|
+
- `DataClassificationGate` to classify traffic as `public`, `internal`, `confidential`, or `restricted`
|
|
156
|
+
- `ProviderRoutingPolicy` to keep sensitive classes on approved providers
|
|
157
|
+
- `ApprovalInboxModel` and `UploadQuarantineWorkflow` for quarantine and review-first intake
|
|
158
|
+
- `buildComplianceEventBundle()` and `sanitizeAuditEvent()` for audit-safe event export
|
|
159
|
+
- `RetrievalTrustScorer` and `OutboundCommunicationGuard` for retrieval trust and outbound checks
|
|
160
|
+
- `detectOperationalDrift()` for release-over-release noise monitoring
|
|
140
161
|
|
|
141
162
|
### `AuditTrail`
|
|
142
163
|
|
|
143
164
|
Use it to record signed events, summarize security activity, and power dashboards or downstream analysis.
|
|
144
165
|
|
|
166
|
+
### Advanced Agent Controls
|
|
167
|
+
|
|
168
|
+
- `ValueAtRiskCircuitBreaker` for financial or high-value operational actions
|
|
169
|
+
- `ShadowConsensusAuditor` for second-model or secondary-review logic conflict checks
|
|
170
|
+
- `CrossModelConsensusWrapper` for automatic cross-model verification of high-impact actions
|
|
171
|
+
- `DigitalTwinOrchestrator` for mock tool environments and sandbox simulations
|
|
172
|
+
- `PolicyLearningLoop` plus `suggestPolicyOverride()` for narrow false-positive tuning suggestions after HITL approvals
|
|
173
|
+
- `AgentIdentityRegistry.issueSignedPassport()` and `issuePassportToken()` for signed agent identity exchange
|
|
174
|
+
|
|
145
175
|
## Example Workflows
|
|
146
176
|
|
|
147
177
|
### Guard a request before calling the model
|
|
@@ -201,6 +231,64 @@ function createModelShield(shield) {
|
|
|
201
231
|
}
|
|
202
232
|
```
|
|
203
233
|
|
|
234
|
+
### Add SSO-aware telemetry and Power BI export
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
const { BlackwallShield, PowerBIExporter } = require('@vpdeva/blackwall-llm-shield-js');
|
|
238
|
+
|
|
239
|
+
const shield = new BlackwallShield({
|
|
240
|
+
identityResolver: (metadata) => ({
|
|
241
|
+
userId: metadata.sso?.subject,
|
|
242
|
+
userEmail: metadata.sso?.email,
|
|
243
|
+
userName: metadata.sso?.displayName,
|
|
244
|
+
identityProvider: metadata.sso?.provider,
|
|
245
|
+
groups: metadata.sso?.groups,
|
|
246
|
+
}),
|
|
247
|
+
telemetryExporters: [
|
|
248
|
+
new PowerBIExporter({ endpointUrl: process.env.POWER_BI_PUSH_URL }),
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Protect high-value actions with a VaR breaker and consensus auditor
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
const firewall = new ToolPermissionFirewall({
|
|
257
|
+
allowedTools: ['issueRefund'],
|
|
258
|
+
valueAtRiskCircuitBreaker: new ValueAtRiskCircuitBreaker({ maxValuePerWindow: 5000 }),
|
|
259
|
+
consensusAuditor: new ShadowConsensusAuditor(),
|
|
260
|
+
consensusRequiredFor: ['issueRefund'],
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Add automatic cross-model consensus
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
const consensus = new CrossModelConsensusWrapper({
|
|
268
|
+
auditorAdapter: geminiAuditorAdapter,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const firewall = new ToolPermissionFirewall({
|
|
272
|
+
allowedTools: ['issueRefund'],
|
|
273
|
+
crossModelConsensus: consensus,
|
|
274
|
+
consensusRequiredFor: ['issueRefund'],
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Generate a digital twin for sandbox testing
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
const twin = new DigitalTwinOrchestrator({
|
|
282
|
+
toolSchemas: [
|
|
283
|
+
{ name: 'lookupOrder', mockResponse: { orderId: 'ord_1', status: 'mocked' } },
|
|
284
|
+
],
|
|
285
|
+
}).generate();
|
|
286
|
+
|
|
287
|
+
await twin.simulateCall('lookupOrder', { orderId: 'ord_1' });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
You can also derive a digital twin from `ToolPermissionFirewall` tool schemas with `DigitalTwinOrchestrator.fromToolPermissionFirewall(firewall)`.
|
|
291
|
+
|
|
204
292
|
### Protect a strict JSON workflow
|
|
205
293
|
|
|
206
294
|
```js
|
|
@@ -319,6 +407,8 @@ const { summarizeOperationalTelemetry } = require('@vpdeva/blackwall-llm-shield-
|
|
|
319
407
|
const summary = summarizeOperationalTelemetry(events);
|
|
320
408
|
console.log(summary.byRoute);
|
|
321
409
|
console.log(summary.byFeature);
|
|
410
|
+
console.log(summary.byUser);
|
|
411
|
+
console.log(summary.byIdentityProvider);
|
|
322
412
|
console.log(summary.noisiestRoutes);
|
|
323
413
|
console.log(summary.weeklyBlockEstimate);
|
|
324
414
|
console.log(summary.highestSeverity);
|
|
@@ -372,6 +462,8 @@ For Gemini-heavy apps, the bundled adapter now preserves system instructions plu
|
|
|
372
462
|
- 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
463
|
- 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
464
|
- For broader approval, focus rollout reviews on false-positive rates, noisiest routes, and latency budgets alongside jailbreak coverage.
|
|
465
|
+
- For executive or staff-facing workflows, always attach authenticated identity metadata so telemetry can answer which user triggered which risky request or output event.
|
|
466
|
+
- 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
467
|
|
|
376
468
|
## Release Commands
|
|
377
469
|
|
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,66 @@ 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 CrossModelConsensusWrapper {
|
|
96
|
+
constructor(options?: Record<string, unknown>);
|
|
97
|
+
evaluate(input?: Record<string, unknown>): Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class DigitalTwinOrchestrator {
|
|
101
|
+
constructor(options?: Record<string, unknown>);
|
|
102
|
+
generate(): Record<string, unknown>;
|
|
103
|
+
static fromToolPermissionFirewall(firewall: unknown): DigitalTwinOrchestrator;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class DataClassificationGate {
|
|
107
|
+
constructor(options?: Record<string, unknown>);
|
|
108
|
+
classify(input?: Record<string, unknown>): string;
|
|
109
|
+
inspect(input?: Record<string, unknown>): Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class ProviderRoutingPolicy {
|
|
113
|
+
constructor(options?: Record<string, unknown>);
|
|
114
|
+
choose(input?: Record<string, unknown>): Record<string, unknown>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class ApprovalInboxModel {
|
|
118
|
+
constructor(options?: Record<string, unknown>);
|
|
119
|
+
createRequest(input?: Record<string, unknown>): Record<string, unknown>;
|
|
120
|
+
approve(id: string, approver: string): Record<string, unknown> | null;
|
|
121
|
+
summarize(): Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class RetrievalTrustScorer {
|
|
125
|
+
score(documents?: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class OutboundCommunicationGuard {
|
|
129
|
+
constructor(options?: Record<string, unknown>);
|
|
130
|
+
inspect(input?: Record<string, unknown>): Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class UploadQuarantineWorkflow {
|
|
134
|
+
constructor(options?: Record<string, unknown>);
|
|
135
|
+
inspectUpload(input?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class PolicyLearningLoop {
|
|
139
|
+
constructor();
|
|
140
|
+
recordDecision(input?: Record<string, unknown>): Record<string, unknown> | null;
|
|
141
|
+
suggestOverrides(): Array<Record<string, unknown>>;
|
|
142
|
+
}
|
|
143
|
+
|
|
82
144
|
export class RetrievalSanitizer {
|
|
83
145
|
constructor(options?: Record<string, unknown>);
|
|
84
146
|
sanitizeDocuments(documents: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
|
|
@@ -97,6 +159,17 @@ export const POLICY_PACKS: Record<string, Record<string, unknown>>;
|
|
|
97
159
|
export function buildShieldOptions(options?: Record<string, unknown>): Record<string, unknown>;
|
|
98
160
|
export function summarizeOperationalTelemetry(events?: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
99
161
|
export function parseJsonOutput(output: unknown): unknown;
|
|
162
|
+
export function normalizeIdentityMetadata(metadata?: Record<string, unknown>, resolver?: ((metadata: Record<string, unknown>) => Record<string, unknown> | null) | null): Record<string, unknown>;
|
|
163
|
+
export function buildEnterpriseTelemetryEvent(event?: Record<string, unknown>, resolver?: ((metadata: Record<string, unknown>) => Record<string, unknown> | null) | null): Record<string, unknown>;
|
|
164
|
+
export function buildPowerBIRecord(event?: Record<string, unknown>): Record<string, unknown>;
|
|
165
|
+
export function buildComplianceEventBundle(event?: Record<string, unknown>): Record<string, unknown>;
|
|
166
|
+
export function sanitizeAuditEvent(event?: Record<string, unknown>, options?: Record<string, unknown>): Record<string, unknown>;
|
|
167
|
+
export function detectOperationalDrift(previousSummary?: Record<string, unknown>, currentSummary?: Record<string, unknown>): Record<string, unknown>;
|
|
168
|
+
export function suggestPolicyOverride(input?: Record<string, unknown>): Record<string, unknown> | null;
|
|
169
|
+
export class PowerBIExporter {
|
|
170
|
+
constructor(options?: Record<string, unknown>);
|
|
171
|
+
send(events?: Array<Record<string, unknown>> | Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
|
|
172
|
+
}
|
|
100
173
|
|
|
101
174
|
export function createOpenAIAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
102
175
|
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.3",
|
|
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)",
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"nextjs",
|
|
72
72
|
"node"
|
|
73
73
|
],
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@xenova/transformers": "^2.17.2"
|
|
76
|
+
},
|
|
74
77
|
"devDependencies": {
|
|
75
78
|
"@changesets/cli": "^2.29.6"
|
|
76
79
|
}
|
package/src/index.js
CHANGED
|
@@ -173,6 +173,41 @@ const SHIELD_PRESETS = {
|
|
|
173
173
|
shadowMode: false,
|
|
174
174
|
policyPack: 'finance',
|
|
175
175
|
},
|
|
176
|
+
governmentStrict: {
|
|
177
|
+
blockOnPromptInjection: true,
|
|
178
|
+
promptInjectionThreshold: 'medium',
|
|
179
|
+
notifyOnRiskLevel: 'medium',
|
|
180
|
+
shadowMode: false,
|
|
181
|
+
policyPack: 'government',
|
|
182
|
+
},
|
|
183
|
+
bankingPayments: {
|
|
184
|
+
blockOnPromptInjection: true,
|
|
185
|
+
promptInjectionThreshold: 'medium',
|
|
186
|
+
notifyOnRiskLevel: 'medium',
|
|
187
|
+
shadowMode: false,
|
|
188
|
+
policyPack: 'finance',
|
|
189
|
+
},
|
|
190
|
+
documentIntake: {
|
|
191
|
+
blockOnPromptInjection: true,
|
|
192
|
+
promptInjectionThreshold: 'high',
|
|
193
|
+
notifyOnRiskLevel: 'medium',
|
|
194
|
+
shadowMode: true,
|
|
195
|
+
policyPack: 'government',
|
|
196
|
+
},
|
|
197
|
+
citizenServices: {
|
|
198
|
+
blockOnPromptInjection: true,
|
|
199
|
+
promptInjectionThreshold: 'medium',
|
|
200
|
+
notifyOnRiskLevel: 'medium',
|
|
201
|
+
shadowMode: true,
|
|
202
|
+
policyPack: 'government',
|
|
203
|
+
},
|
|
204
|
+
internalOpsAgent: {
|
|
205
|
+
blockOnPromptInjection: true,
|
|
206
|
+
promptInjectionThreshold: 'medium',
|
|
207
|
+
notifyOnRiskLevel: 'medium',
|
|
208
|
+
shadowMode: true,
|
|
209
|
+
policyPack: 'finance',
|
|
210
|
+
},
|
|
176
211
|
};
|
|
177
212
|
|
|
178
213
|
const CORE_INTERFACE_VERSION = '1.0';
|
|
@@ -372,6 +407,250 @@ function createTelemetryEvent(type, payload = {}) {
|
|
|
372
407
|
};
|
|
373
408
|
}
|
|
374
409
|
|
|
410
|
+
function normalizeIdentityMetadata(metadata = {}, resolver = null) {
|
|
411
|
+
const resolved = typeof resolver === 'function' ? resolver(metadata) || {} : {};
|
|
412
|
+
const source = { ...metadata, ...resolved };
|
|
413
|
+
const groups = Array.isArray(source.groups)
|
|
414
|
+
? source.groups
|
|
415
|
+
: (Array.isArray(source.ssoGroups) ? source.ssoGroups : (typeof source.groups === 'string' ? source.groups.split(',').map((item) => item.trim()).filter(Boolean) : []));
|
|
416
|
+
return {
|
|
417
|
+
userId: source.userId || source.user_id || source.subject || source.sub || 'anonymous',
|
|
418
|
+
userEmail: source.userEmail || source.user_email || source.email || source.upn || null,
|
|
419
|
+
userName: source.userName || source.user_name || source.displayName || source.display_name || source.name || null,
|
|
420
|
+
tenantId: source.tenantId || source.tenant_id || source.orgId || source.org_id || 'default',
|
|
421
|
+
identityProvider: source.identityProvider || source.identity_provider || source.ssoProvider || source.sso_provider || source.idp || null,
|
|
422
|
+
authMethod: source.authMethod || source.auth_method || source.authType || source.auth_type || null,
|
|
423
|
+
sessionId: source.sessionId || source.session_id || null,
|
|
424
|
+
groups,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildEnterpriseTelemetryEvent(event = {}, resolver = null) {
|
|
429
|
+
const metadata = event && event.metadata ? event.metadata : {};
|
|
430
|
+
const actor = normalizeIdentityMetadata(metadata, resolver);
|
|
431
|
+
return {
|
|
432
|
+
...event,
|
|
433
|
+
actor,
|
|
434
|
+
metadata: {
|
|
435
|
+
...metadata,
|
|
436
|
+
userId: metadata.userId || metadata.user_id || actor.userId,
|
|
437
|
+
tenantId: metadata.tenantId || metadata.tenant_id || actor.tenantId,
|
|
438
|
+
identityProvider: metadata.identityProvider || metadata.identity_provider || actor.identityProvider,
|
|
439
|
+
sessionId: metadata.sessionId || metadata.session_id || actor.sessionId,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function buildPowerBIRecord(event = {}) {
|
|
445
|
+
const actor = event.actor || normalizeIdentityMetadata(event.metadata || {});
|
|
446
|
+
const metadata = event.metadata || {};
|
|
447
|
+
return {
|
|
448
|
+
eventType: event.type || 'unknown',
|
|
449
|
+
createdAt: event.createdAt || new Date().toISOString(),
|
|
450
|
+
route: metadata.route || metadata.path || 'unknown',
|
|
451
|
+
feature: metadata.feature || metadata.capability || metadata.route || 'unknown',
|
|
452
|
+
model: metadata.model || metadata.modelName || 'unknown',
|
|
453
|
+
tenantId: actor.tenantId || 'default',
|
|
454
|
+
userId: actor.userId || 'anonymous',
|
|
455
|
+
userEmail: actor.userEmail || null,
|
|
456
|
+
userName: actor.userName || null,
|
|
457
|
+
identityProvider: actor.identityProvider || null,
|
|
458
|
+
authMethod: actor.authMethod || null,
|
|
459
|
+
sessionId: actor.sessionId || null,
|
|
460
|
+
blocked: !!event.blocked,
|
|
461
|
+
shadowMode: !!event.shadowMode,
|
|
462
|
+
severity: (event.report && event.report.outputReview && event.report.outputReview.severity)
|
|
463
|
+
|| (event.report && event.report.promptInjection && event.report.promptInjection.level)
|
|
464
|
+
|| 'low',
|
|
465
|
+
topRule: (event.report && event.report.promptInjection && Array.isArray(event.report.promptInjection.matches) && event.report.promptInjection.matches[0] && event.report.promptInjection.matches[0].id) || null,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
class PowerBIExporter {
|
|
470
|
+
constructor(options = {}) {
|
|
471
|
+
this.endpointUrl = options.endpointUrl || options.webhookUrl || null;
|
|
472
|
+
this.fetchImpl = options.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async send(events = []) {
|
|
476
|
+
const records = (Array.isArray(events) ? events : [events]).filter(Boolean).map((event) => buildPowerBIRecord(event));
|
|
477
|
+
if (!this.endpointUrl || !this.fetchImpl) return records;
|
|
478
|
+
await this.fetchImpl(this.endpointUrl, {
|
|
479
|
+
method: 'POST',
|
|
480
|
+
headers: { 'Content-Type': 'application/json' },
|
|
481
|
+
body: JSON.stringify(records),
|
|
482
|
+
});
|
|
483
|
+
return records;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
class DataClassificationGate {
|
|
488
|
+
constructor(options = {}) {
|
|
489
|
+
this.defaultLevel = options.defaultLevel || 'internal';
|
|
490
|
+
this.providerAllowMap = options.providerAllowMap || {};
|
|
491
|
+
this.levelOrder = ['public', 'internal', 'confidential', 'restricted'];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
classify({ metadata = {}, findings = [], messages = [] } = {}) {
|
|
495
|
+
if (metadata.classification) return metadata.classification;
|
|
496
|
+
const types = findings.map((item) => item.type).filter(Boolean);
|
|
497
|
+
if (types.some((type) => ['creditCard', 'tfn', 'passport', 'license', 'apiKey', 'jwt', 'bearerToken'].includes(type))) return 'restricted';
|
|
498
|
+
if (types.length) return 'confidential';
|
|
499
|
+
const text = JSON.stringify(messages || []).toLowerCase();
|
|
500
|
+
if (/\bconfidential|restricted|secret\b/.test(text)) return 'confidential';
|
|
501
|
+
return this.defaultLevel;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
inspect({ metadata = {}, findings = [], messages = [], provider = null } = {}) {
|
|
505
|
+
const classification = this.classify({ metadata, findings, messages });
|
|
506
|
+
const allowedProviders = this.providerAllowMap[classification] || null;
|
|
507
|
+
const allowed = !provider || !allowedProviders || allowedProviders.includes(provider);
|
|
508
|
+
return {
|
|
509
|
+
allowed,
|
|
510
|
+
classification,
|
|
511
|
+
provider,
|
|
512
|
+
allowedProviders,
|
|
513
|
+
reason: allowed ? null : `Provider ${provider} is not allowed for ${classification} data`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
class ProviderRoutingPolicy {
|
|
519
|
+
constructor(options = {}) {
|
|
520
|
+
this.routes = options.routes || {};
|
|
521
|
+
this.fallbackProvider = options.fallbackProvider || null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
choose({ route = '', classification = 'internal', requestedProvider = null, candidates = [] } = {}) {
|
|
525
|
+
const routeConfig = this.routes[route] || this.routes.default || {};
|
|
526
|
+
const preferred = routeConfig[classification] || routeConfig.default || requestedProvider || this.fallbackProvider || candidates[0] || null;
|
|
527
|
+
const allowedCandidates = candidates.length ? candidates : [preferred].filter(Boolean);
|
|
528
|
+
const chosen = allowedCandidates.includes(preferred) ? preferred : allowedCandidates[0] || null;
|
|
529
|
+
return { provider: chosen, route, classification, requestedProvider, candidates: allowedCandidates };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
class ApprovalInboxModel {
|
|
534
|
+
constructor(options = {}) {
|
|
535
|
+
this.requests = [];
|
|
536
|
+
this.requiredApprovers = options.requiredApprovers || 1;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
createRequest(request = {}) {
|
|
540
|
+
const id = request.id || `apr_${crypto.randomBytes(6).toString('hex')}`;
|
|
541
|
+
const record = {
|
|
542
|
+
id,
|
|
543
|
+
status: 'pending',
|
|
544
|
+
requiredApprovers: request.requiredApprovers || this.requiredApprovers,
|
|
545
|
+
approvals: [],
|
|
546
|
+
createdAt: new Date().toISOString(),
|
|
547
|
+
...request,
|
|
548
|
+
};
|
|
549
|
+
this.requests.push(record);
|
|
550
|
+
return record;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
approve(id, approver) {
|
|
554
|
+
const request = this.requests.find((item) => item.id === id);
|
|
555
|
+
if (!request) return null;
|
|
556
|
+
if (!request.approvals.includes(approver)) request.approvals.push(approver);
|
|
557
|
+
request.status = request.approvals.length >= request.requiredApprovers ? 'approved' : 'pending';
|
|
558
|
+
return request;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
summarize() {
|
|
562
|
+
return {
|
|
563
|
+
total: this.requests.length,
|
|
564
|
+
pending: this.requests.filter((item) => item.status === 'pending').length,
|
|
565
|
+
approved: this.requests.filter((item) => item.status === 'approved').length,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function buildComplianceEventBundle(event = {}) {
|
|
571
|
+
const payload = JSON.stringify(event);
|
|
572
|
+
const evidenceHash = crypto.createHash('sha256').update(payload).digest('hex');
|
|
573
|
+
return {
|
|
574
|
+
schemaVersion: '1.0',
|
|
575
|
+
generatedAt: new Date().toISOString(),
|
|
576
|
+
evidenceHash,
|
|
577
|
+
event,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function sanitizeAuditEvent(event = {}, options = {}) {
|
|
582
|
+
const keepEvidence = !!options.keepEvidence;
|
|
583
|
+
const clone = JSON.parse(JSON.stringify(event || {}));
|
|
584
|
+
if (!keepEvidence && clone.report && clone.report.sensitiveData) {
|
|
585
|
+
clone.report.sensitiveData.findings = (clone.report.sensitiveData.findings || []).map((finding) => ({ type: finding.type }));
|
|
586
|
+
}
|
|
587
|
+
return clone;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
class RetrievalTrustScorer {
|
|
591
|
+
score(documents = []) {
|
|
592
|
+
return (documents || []).map((doc, index) => {
|
|
593
|
+
const metadata = doc && doc.metadata ? doc.metadata : {};
|
|
594
|
+
const sourceTrust = metadata.approved ? 0.4 : 0.1;
|
|
595
|
+
const freshness = metadata.fresh ? 0.3 : 0.1;
|
|
596
|
+
const origin = metadata.origin === 'trusted' ? 0.3 : 0.1;
|
|
597
|
+
const score = Number(Math.min(1, sourceTrust + freshness + origin).toFixed(2));
|
|
598
|
+
return {
|
|
599
|
+
id: doc && doc.id ? doc.id : `doc_${index + 1}`,
|
|
600
|
+
trustScore: score,
|
|
601
|
+
trusted: score >= 0.7,
|
|
602
|
+
metadata,
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
class OutboundCommunicationGuard {
|
|
609
|
+
constructor(options = {}) {
|
|
610
|
+
this.outputFirewall = options.outputFirewall || new OutputFirewall({ riskThreshold: 'high', enforceProfessionalTone: true });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
inspect({ message, metadata = {} } = {}) {
|
|
614
|
+
const review = this.outputFirewall.inspect(message, {});
|
|
615
|
+
return {
|
|
616
|
+
allowed: review.allowed,
|
|
617
|
+
review,
|
|
618
|
+
channel: metadata.channel || 'outbound',
|
|
619
|
+
recipient: metadata.recipient || null,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
class UploadQuarantineWorkflow {
|
|
625
|
+
constructor(options = {}) {
|
|
626
|
+
this.shield = options.shield || new BlackwallShield({ preset: 'documentIntake' });
|
|
627
|
+
this.inbox = options.approvalInbox || new ApprovalInboxModel({ requiredApprovers: 1 });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async inspectUpload({ content, metadata = {} } = {}) {
|
|
631
|
+
const guarded = await this.shield.guardModelRequest({
|
|
632
|
+
messages: [{ role: 'user', content }],
|
|
633
|
+
metadata: { ...metadata, feature: metadata.feature || 'upload_intake' },
|
|
634
|
+
});
|
|
635
|
+
const quarantined = !guarded.allowed || guarded.report.sensitiveData.hasSensitiveData;
|
|
636
|
+
const approvalRequest = quarantined ? this.inbox.createRequest({ route: metadata.route || '/uploads', reason: guarded.reason || 'Upload requires review', metadata }) : null;
|
|
637
|
+
return { quarantined, approvalRequest, guard: guarded };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function detectOperationalDrift(previousSummary = {}, currentSummary = {}) {
|
|
642
|
+
const previousBlocked = previousSummary.weeklyBlockEstimate || 0;
|
|
643
|
+
const currentBlocked = currentSummary.weeklyBlockEstimate || 0;
|
|
644
|
+
const delta = currentBlocked - previousBlocked;
|
|
645
|
+
return {
|
|
646
|
+
driftDetected: Math.abs(delta) > 0,
|
|
647
|
+
blockedDelta: delta,
|
|
648
|
+
previousBlocked,
|
|
649
|
+
currentBlocked,
|
|
650
|
+
severity: delta > 10 ? 'high' : delta > 0 ? 'medium' : 'low',
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
375
654
|
function summarizeOperationalTelemetry(events = []) {
|
|
376
655
|
const summary = {
|
|
377
656
|
totalEvents: 0,
|
|
@@ -380,6 +659,8 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
380
659
|
byType: {},
|
|
381
660
|
byRoute: {},
|
|
382
661
|
byFeature: {},
|
|
662
|
+
byUser: {},
|
|
663
|
+
byIdentityProvider: {},
|
|
383
664
|
byTenant: {},
|
|
384
665
|
byModel: {},
|
|
385
666
|
byPolicyOutcome: {
|
|
@@ -398,6 +679,8 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
398
679
|
const route = metadata.route || metadata.path || 'unknown';
|
|
399
680
|
const feature = metadata.feature || metadata.capability || route;
|
|
400
681
|
const tenant = metadata.tenantId || metadata.tenant_id || 'unknown';
|
|
682
|
+
const user = metadata.userId || metadata.user_id || (event.actor && event.actor.userId) || 'unknown';
|
|
683
|
+
const idp = metadata.identityProvider || metadata.identity_provider || (event.actor && event.actor.identityProvider) || 'unknown';
|
|
401
684
|
const model = metadata.model || metadata.modelName || 'unknown';
|
|
402
685
|
const severity = event && event.report && event.report.outputReview
|
|
403
686
|
? event.report.outputReview.severity
|
|
@@ -406,6 +689,8 @@ function summarizeOperationalTelemetry(events = []) {
|
|
|
406
689
|
summary.byType[type] = (summary.byType[type] || 0) + 1;
|
|
407
690
|
summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
|
|
408
691
|
summary.byFeature[feature] = (summary.byFeature[feature] || 0) + 1;
|
|
692
|
+
summary.byUser[user] = (summary.byUser[user] || 0) + 1;
|
|
693
|
+
summary.byIdentityProvider[idp] = (summary.byIdentityProvider[idp] || 0) + 1;
|
|
409
694
|
summary.byTenant[tenant] = (summary.byTenant[tenant] || 0) + 1;
|
|
410
695
|
summary.byModel[model] = (summary.byModel[model] || 0) + 1;
|
|
411
696
|
if (event && event.blocked) summary.blockedEvents += 1;
|
|
@@ -1112,6 +1397,8 @@ class BlackwallShield {
|
|
|
1112
1397
|
outputFirewallDefaults: {},
|
|
1113
1398
|
onAlert: null,
|
|
1114
1399
|
onTelemetry: null,
|
|
1400
|
+
telemetryExporters: [],
|
|
1401
|
+
identityResolver: null,
|
|
1115
1402
|
webhookUrl: null,
|
|
1116
1403
|
...options,
|
|
1117
1404
|
};
|
|
@@ -1143,8 +1430,15 @@ class BlackwallShield {
|
|
|
1143
1430
|
}
|
|
1144
1431
|
|
|
1145
1432
|
async emitTelemetry(event) {
|
|
1433
|
+
const enriched = buildEnterpriseTelemetryEvent(event, this.options.identityResolver);
|
|
1146
1434
|
if (typeof this.options.onTelemetry === 'function') {
|
|
1147
|
-
await this.options.onTelemetry(
|
|
1435
|
+
await this.options.onTelemetry(enriched);
|
|
1436
|
+
}
|
|
1437
|
+
const exporters = Array.isArray(this.options.telemetryExporters) ? this.options.telemetryExporters : [];
|
|
1438
|
+
for (const exporter of exporters) {
|
|
1439
|
+
if (exporter && typeof exporter.send === 'function') {
|
|
1440
|
+
await exporter.send([enriched]);
|
|
1441
|
+
}
|
|
1148
1442
|
}
|
|
1149
1443
|
}
|
|
1150
1444
|
|
|
@@ -1563,9 +1857,10 @@ class CoTScanner {
|
|
|
1563
1857
|
}
|
|
1564
1858
|
|
|
1565
1859
|
class AgentIdentityRegistry {
|
|
1566
|
-
constructor() {
|
|
1860
|
+
constructor(options = {}) {
|
|
1567
1861
|
this.identities = new Map();
|
|
1568
1862
|
this.ephemeralTokens = new Map();
|
|
1863
|
+
this.secret = options.secret || 'blackwall-agent-passport-secret';
|
|
1569
1864
|
}
|
|
1570
1865
|
|
|
1571
1866
|
register(agentId, profile = {}) {
|
|
@@ -1595,6 +1890,256 @@ class AgentIdentityRegistry {
|
|
|
1595
1890
|
}
|
|
1596
1891
|
return { valid: true, agentId: record.agentId };
|
|
1597
1892
|
}
|
|
1893
|
+
|
|
1894
|
+
issueSignedPassport(agentId, options = {}) {
|
|
1895
|
+
const identity = this.get(agentId) || this.register(agentId, options.profile || {});
|
|
1896
|
+
const securityScore = options.securityScore != null
|
|
1897
|
+
? options.securityScore
|
|
1898
|
+
: Math.max(0, 100 - (Object.values(identity.capabilities || {}).filter(Boolean).length * 10));
|
|
1899
|
+
const passport = {
|
|
1900
|
+
agentId,
|
|
1901
|
+
issuedAt: new Date().toISOString(),
|
|
1902
|
+
issuer: options.issuer || 'blackwall-llm-shield-js',
|
|
1903
|
+
blackwallProtected: options.blackwallProtected !== false,
|
|
1904
|
+
securityScore,
|
|
1905
|
+
scopes: identity.scopes || [],
|
|
1906
|
+
persona: identity.persona || 'default',
|
|
1907
|
+
environment: options.environment || 'production',
|
|
1908
|
+
};
|
|
1909
|
+
const signature = crypto.createHmac('sha256', this.secret).update(JSON.stringify(passport)).digest('hex');
|
|
1910
|
+
return { ...passport, signature };
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
verifySignedPassport(passport = {}) {
|
|
1914
|
+
const { signature, ...unsigned } = passport || {};
|
|
1915
|
+
if (!signature) return { valid: false, reason: 'Passport signature is required' };
|
|
1916
|
+
const expected = crypto.createHmac('sha256', this.secret).update(JSON.stringify(unsigned)).digest('hex');
|
|
1917
|
+
return {
|
|
1918
|
+
valid: crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)),
|
|
1919
|
+
agentId: unsigned.agentId || null,
|
|
1920
|
+
securityScore: unsigned.securityScore || null,
|
|
1921
|
+
blackwallProtected: !!unsigned.blackwallProtected,
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
issuePassportToken(agentId, options = {}) {
|
|
1926
|
+
const passport = this.issueSignedPassport(agentId, options);
|
|
1927
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
1928
|
+
const payload = Buffer.from(JSON.stringify(passport)).toString('base64url');
|
|
1929
|
+
const signature = crypto.createHmac('sha256', this.secret).update(`${header}.${payload}`).digest('base64url');
|
|
1930
|
+
return `${header}.${payload}.${signature}`;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
verifyPassportToken(token) {
|
|
1934
|
+
if (!token || typeof token !== 'string' || token.split('.').length !== 3) {
|
|
1935
|
+
return { valid: false, reason: 'Malformed passport token' };
|
|
1936
|
+
}
|
|
1937
|
+
const [header, payload, signature] = token.split('.');
|
|
1938
|
+
const expected = crypto.createHmac('sha256', this.secret).update(`${header}.${payload}`).digest('base64url');
|
|
1939
|
+
if (signature !== expected) return { valid: false, reason: 'Invalid passport token signature' };
|
|
1940
|
+
const passport = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
1941
|
+
return { valid: true, passport, ...this.verifySignedPassport(passport) };
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
class ValueAtRiskCircuitBreaker {
|
|
1946
|
+
constructor(options = {}) {
|
|
1947
|
+
this.maxValuePerWindow = options.maxValuePerWindow || 5000;
|
|
1948
|
+
this.windowMs = options.windowMs || (60 * 60 * 1000);
|
|
1949
|
+
this.revocationMs = options.revocationMs || (30 * 60 * 1000);
|
|
1950
|
+
this.valueExtractor = options.valueExtractor || ((args = {}, context = {}) => Number(context.actionValue != null ? context.actionValue : (args.amount != null ? args.amount : 0)));
|
|
1951
|
+
this.toolSchemas = options.toolSchemas || [];
|
|
1952
|
+
this.entries = [];
|
|
1953
|
+
this.revocations = new Map();
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
revokeSession(sessionId, durationMs = this.revocationMs) {
|
|
1957
|
+
if (!sessionId) return null;
|
|
1958
|
+
const expiresAt = Date.now() + durationMs;
|
|
1959
|
+
this.revocations.set(sessionId, expiresAt);
|
|
1960
|
+
return { sessionId, revokedUntil: new Date(expiresAt).toISOString() };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
inspect({ tool, args = {}, context = {} } = {}) {
|
|
1964
|
+
const sessionId = context.sessionId || context.session_id || null;
|
|
1965
|
+
const now = Date.now();
|
|
1966
|
+
const revokedUntil = sessionId ? this.revocations.get(sessionId) : null;
|
|
1967
|
+
if (revokedUntil && revokedUntil > now) {
|
|
1968
|
+
return {
|
|
1969
|
+
allowed: false,
|
|
1970
|
+
triggered: true,
|
|
1971
|
+
requiresMfa: true,
|
|
1972
|
+
reason: 'Session is revoked until MFA or human review completes',
|
|
1973
|
+
revokedSession: sessionId,
|
|
1974
|
+
revokedUntil: new Date(revokedUntil).toISOString(),
|
|
1975
|
+
riskWindowValue: null,
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
this.entries = this.entries.filter((entry) => (now - entry.at) <= this.windowMs);
|
|
1979
|
+
const schema = this.toolSchemas.find((item) => item && item.name === tool) || {};
|
|
1980
|
+
const field = schema.monetaryValueField || schema.valueField || null;
|
|
1981
|
+
const schemaValue = field ? Number(args && args[field]) : 0;
|
|
1982
|
+
const actionValue = Math.max(0, Number((schemaValue || this.valueExtractor(args, context)) || 0));
|
|
1983
|
+
const key = sessionId || context.agentId || context.agent_id || context.userId || context.user_id || 'default';
|
|
1984
|
+
const relevant = this.entries.filter((entry) => entry.key === key);
|
|
1985
|
+
const riskWindowValue = relevant.reduce((sum, entry) => sum + entry.value, 0) + actionValue;
|
|
1986
|
+
const triggered = riskWindowValue > this.maxValuePerWindow;
|
|
1987
|
+
if (triggered) {
|
|
1988
|
+
const revocation = this.revokeSession(sessionId);
|
|
1989
|
+
return {
|
|
1990
|
+
allowed: false,
|
|
1991
|
+
triggered: true,
|
|
1992
|
+
requiresMfa: true,
|
|
1993
|
+
reason: `Value-at-risk threshold exceeded for ${tool || 'action'}`,
|
|
1994
|
+
riskWindowValue,
|
|
1995
|
+
threshold: this.maxValuePerWindow,
|
|
1996
|
+
actionValue,
|
|
1997
|
+
revokedSession: revocation && revocation.sessionId,
|
|
1998
|
+
revokedUntil: revocation && revocation.revokedUntil,
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
this.entries.push({ key, tool: tool || 'unknown', value: actionValue, at: now });
|
|
2002
|
+
return {
|
|
2003
|
+
allowed: true,
|
|
2004
|
+
triggered: false,
|
|
2005
|
+
requiresMfa: false,
|
|
2006
|
+
riskWindowValue,
|
|
2007
|
+
threshold: this.maxValuePerWindow,
|
|
2008
|
+
actionValue,
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
class ShadowConsensusAuditor {
|
|
2014
|
+
constructor(options = {}) {
|
|
2015
|
+
this.review = options.review || ((payload = {}) => {
|
|
2016
|
+
const text = JSON.stringify({ tool: payload.tool, args: payload.args, sessionContext: payload.sessionContext || '' }).toLowerCase();
|
|
2017
|
+
const disagreement = /\b(ignore previous|bypass|override|secret|reveal)\b/i.test(text);
|
|
2018
|
+
return {
|
|
2019
|
+
agreed: !disagreement,
|
|
2020
|
+
disagreement,
|
|
2021
|
+
reason: disagreement ? 'Logic Conflict: shadow auditor found risky reasoning drift' : null,
|
|
2022
|
+
};
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
inspect(payload = {}) {
|
|
2027
|
+
const result = this.review(payload) || {};
|
|
2028
|
+
return {
|
|
2029
|
+
agreed: result.agreed !== false,
|
|
2030
|
+
disagreement: !!result.disagreement || result.agreed === false,
|
|
2031
|
+
reason: result.reason || (result.agreed === false ? 'Logic Conflict detected by shadow auditor' : null),
|
|
2032
|
+
auditor: result.auditor || 'shadow',
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
class CrossModelConsensusWrapper {
|
|
2038
|
+
constructor(options = {}) {
|
|
2039
|
+
this.primaryAdapter = options.primaryAdapter || null;
|
|
2040
|
+
this.auditorAdapter = options.auditorAdapter || null;
|
|
2041
|
+
this.decisionParser = options.decisionParser || ((output) => {
|
|
2042
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output || '');
|
|
2043
|
+
if (/\b(block|unsafe|deny|disagree)\b/i.test(text)) return 'block';
|
|
2044
|
+
return 'allow';
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
async evaluate({ messages = [], metadata = {}, primaryResult = null } = {}) {
|
|
2049
|
+
if (!this.auditorAdapter || typeof this.auditorAdapter.invoke !== 'function') {
|
|
2050
|
+
return { agreed: true, disagreement: false, reason: null, primaryDecision: 'allow', auditorDecision: 'allow' };
|
|
2051
|
+
}
|
|
2052
|
+
const primaryDecision = primaryResult && primaryResult.blocked ? 'block' : 'allow';
|
|
2053
|
+
const response = await this.auditorAdapter.invoke({ messages, metadata, primaryResult });
|
|
2054
|
+
const output = typeof this.auditorAdapter.extractOutput === 'function'
|
|
2055
|
+
? this.auditorAdapter.extractOutput(response && Object.prototype.hasOwnProperty.call(response, 'response') ? response.response : response)
|
|
2056
|
+
: (response && response.output) || response;
|
|
2057
|
+
const auditorDecision = this.decisionParser(output);
|
|
2058
|
+
const disagreement = auditorDecision !== primaryDecision;
|
|
2059
|
+
return {
|
|
2060
|
+
agreed: !disagreement,
|
|
2061
|
+
disagreement,
|
|
2062
|
+
primaryDecision,
|
|
2063
|
+
auditorDecision,
|
|
2064
|
+
reason: disagreement ? 'Logic Conflict: cross-model auditor disagreed with the primary decision' : null,
|
|
2065
|
+
auditorResponse: response,
|
|
2066
|
+
auditorOutput: output,
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
class DigitalTwinOrchestrator {
|
|
2072
|
+
constructor(options = {}) {
|
|
2073
|
+
this.toolSchemas = options.toolSchemas || [];
|
|
2074
|
+
this.invocations = [];
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
generate() {
|
|
2078
|
+
const handlers = {};
|
|
2079
|
+
for (const schema of this.toolSchemas) {
|
|
2080
|
+
if (!schema || !schema.name) continue;
|
|
2081
|
+
handlers[schema.name] = async (args = {}) => {
|
|
2082
|
+
const response = schema.mockResponse || schema.sampleResponse || { ok: true, tool: schema.name, args };
|
|
2083
|
+
this.invocations.push({ tool: schema.name, args, response, at: new Date().toISOString() });
|
|
2084
|
+
return response;
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
handlers,
|
|
2089
|
+
simulateCall: async (tool, args = {}) => {
|
|
2090
|
+
if (!handlers[tool]) throw new Error(`No digital twin registered for ${tool}`);
|
|
2091
|
+
return handlers[tool](args);
|
|
2092
|
+
},
|
|
2093
|
+
invocations: this.invocations,
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
static fromToolPermissionFirewall(firewall) {
|
|
2098
|
+
const schemas = Array.isArray(firewall && firewall.options && firewall.options.toolSchemas)
|
|
2099
|
+
? firewall.options.toolSchemas
|
|
2100
|
+
: [];
|
|
2101
|
+
return new DigitalTwinOrchestrator({ toolSchemas: schemas });
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function suggestPolicyOverride({ route = null, approval = null, guardResult = null, toolDecision = null } = {}) {
|
|
2106
|
+
if (approval !== true) return null;
|
|
2107
|
+
if (guardResult && guardResult.report && guardResult.report.promptInjection) {
|
|
2108
|
+
const rules = (guardResult.report.promptInjection.matches || []).map((item) => item.id).filter(Boolean);
|
|
2109
|
+
return {
|
|
2110
|
+
route: route || (guardResult.report.metadata && (guardResult.report.metadata.route || guardResult.report.metadata.path)) || '*',
|
|
2111
|
+
options: {
|
|
2112
|
+
shadowMode: true,
|
|
2113
|
+
suppressPromptRules: [...new Set(rules)],
|
|
2114
|
+
},
|
|
2115
|
+
rationale: 'Suggested from approved false positive',
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (toolDecision && toolDecision.approvalRequest) {
|
|
2119
|
+
return {
|
|
2120
|
+
route: route || ((toolDecision.approvalRequest.context || {}).route) || '*',
|
|
2121
|
+
options: {
|
|
2122
|
+
requireHumanApprovalFor: [toolDecision.approvalRequest.tool],
|
|
2123
|
+
},
|
|
2124
|
+
rationale: 'Suggested from approved high-impact tool action',
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
class PolicyLearningLoop {
|
|
2131
|
+
constructor() {
|
|
2132
|
+
this.decisions = [];
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
recordDecision(input = {}) {
|
|
2136
|
+
this.decisions.push({ ...input, recordedAt: new Date().toISOString() });
|
|
2137
|
+
return suggestPolicyOverride(input);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
suggestOverrides() {
|
|
2141
|
+
return this.decisions.map((entry) => suggestPolicyOverride(entry)).filter(Boolean);
|
|
2142
|
+
}
|
|
1598
2143
|
}
|
|
1599
2144
|
|
|
1600
2145
|
class AgenticCapabilityGater {
|
|
@@ -1735,8 +2280,13 @@ class ToolPermissionFirewall {
|
|
|
1735
2280
|
allowedTools: [],
|
|
1736
2281
|
blockedTools: [],
|
|
1737
2282
|
validators: {},
|
|
2283
|
+
toolSchemas: [],
|
|
1738
2284
|
requireHumanApprovalFor: [],
|
|
1739
2285
|
capabilityGater: null,
|
|
2286
|
+
valueAtRiskCircuitBreaker: null,
|
|
2287
|
+
consensusAuditor: null,
|
|
2288
|
+
crossModelConsensus: null,
|
|
2289
|
+
consensusRequiredFor: [],
|
|
1740
2290
|
onApprovalRequest: null,
|
|
1741
2291
|
approvalWebhookUrl: null,
|
|
1742
2292
|
...options,
|
|
@@ -1766,6 +2316,46 @@ class ToolPermissionFirewall {
|
|
|
1766
2316
|
return { allowed: false, reason: gate.reason, requiresApproval: false, agentGate: gate };
|
|
1767
2317
|
}
|
|
1768
2318
|
}
|
|
2319
|
+
if (this.options.valueAtRiskCircuitBreaker) {
|
|
2320
|
+
const breaker = this.options.valueAtRiskCircuitBreaker.inspect({ tool, args, context });
|
|
2321
|
+
if (!breaker.allowed) {
|
|
2322
|
+
return {
|
|
2323
|
+
allowed: false,
|
|
2324
|
+
reason: breaker.reason,
|
|
2325
|
+
requiresApproval: true,
|
|
2326
|
+
requiresMfa: !!breaker.requiresMfa,
|
|
2327
|
+
circuitBreaker: breaker,
|
|
2328
|
+
approvalRequest: { tool, args, context, breaker },
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
if (this.options.consensusAuditor && (context.highImpact || this.options.consensusRequiredFor.includes(tool))) {
|
|
2333
|
+
const consensus = this.options.consensusAuditor.inspect({
|
|
2334
|
+
tool,
|
|
2335
|
+
args,
|
|
2336
|
+
context,
|
|
2337
|
+
sessionContext: context.sessionContext || context.session_buffer || null,
|
|
2338
|
+
});
|
|
2339
|
+
if (consensus.disagreement) {
|
|
2340
|
+
return {
|
|
2341
|
+
allowed: false,
|
|
2342
|
+
reason: consensus.reason || 'Logic Conflict detected by shadow auditor',
|
|
2343
|
+
requiresApproval: true,
|
|
2344
|
+
logicConflict: true,
|
|
2345
|
+
consensus,
|
|
2346
|
+
approvalRequest: { tool, args, context, consensus },
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
if (this.options.crossModelConsensus && (context.highImpact || this.options.consensusRequiredFor.includes(tool))) {
|
|
2351
|
+
return {
|
|
2352
|
+
allowed: false,
|
|
2353
|
+
reason: 'Cross-model consensus requires async inspection',
|
|
2354
|
+
requiresApproval: true,
|
|
2355
|
+
requiresAsyncConsensus: true,
|
|
2356
|
+
approvalRequest: { tool, args, context },
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
1769
2359
|
const requiresApproval = this.options.requireHumanApprovalFor.includes(tool);
|
|
1770
2360
|
return {
|
|
1771
2361
|
allowed: !requiresApproval,
|
|
@@ -1777,6 +2367,24 @@ class ToolPermissionFirewall {
|
|
|
1777
2367
|
|
|
1778
2368
|
async inspectCallAsync(input = {}) {
|
|
1779
2369
|
const result = this.inspectCall(input);
|
|
2370
|
+
if (result.requiresAsyncConsensus && this.options.crossModelConsensus) {
|
|
2371
|
+
const consensus = await this.options.crossModelConsensus.evaluate({
|
|
2372
|
+
messages: input.context && input.context.consensusMessages ? input.context.consensusMessages : [{ role: 'user', content: JSON.stringify({ tool: input.tool, args: input.args, context: input.context }) }],
|
|
2373
|
+
metadata: input.context || {},
|
|
2374
|
+
primaryResult: result,
|
|
2375
|
+
});
|
|
2376
|
+
if (consensus.disagreement) {
|
|
2377
|
+
return {
|
|
2378
|
+
allowed: false,
|
|
2379
|
+
reason: consensus.reason,
|
|
2380
|
+
requiresApproval: true,
|
|
2381
|
+
logicConflict: true,
|
|
2382
|
+
consensus,
|
|
2383
|
+
approvalRequest: { tool: input.tool, args: input.args || {}, context: input.context || {}, consensus },
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
return { allowed: true, reason: null, requiresApproval: false, consensus };
|
|
2387
|
+
}
|
|
1780
2388
|
if (result.requiresApproval) {
|
|
1781
2389
|
if (typeof this.options.onApprovalRequest === 'function') {
|
|
1782
2390
|
await this.options.onApprovalRequest(result.approvalRequest);
|
|
@@ -1860,11 +2468,14 @@ class AuditTrail {
|
|
|
1860
2468
|
constructor(options = {}) {
|
|
1861
2469
|
this.secret = options.secret || 'blackwall-default-secret';
|
|
1862
2470
|
this.events = [];
|
|
2471
|
+
this.identityResolver = options.identityResolver || null;
|
|
1863
2472
|
}
|
|
1864
2473
|
|
|
1865
2474
|
record(event = {}) {
|
|
2475
|
+
const actor = event.actor || normalizeIdentityMetadata(event.metadata || event, this.identityResolver);
|
|
1866
2476
|
const payload = {
|
|
1867
2477
|
...event,
|
|
2478
|
+
actor,
|
|
1868
2479
|
complianceMap: event.complianceMap || mapCompliance([
|
|
1869
2480
|
...(event.ruleIds || []),
|
|
1870
2481
|
event.type === 'retrieval_poisoning_detected' ? 'retrieval_poisoning' : null,
|
|
@@ -2130,14 +2741,20 @@ module.exports = {
|
|
|
2130
2741
|
AuditTrail,
|
|
2131
2742
|
BlackwallShield,
|
|
2132
2743
|
CoTScanner,
|
|
2744
|
+
CrossModelConsensusWrapper,
|
|
2745
|
+
DigitalTwinOrchestrator,
|
|
2133
2746
|
ImageMetadataScanner,
|
|
2134
2747
|
LightweightIntentScorer,
|
|
2135
2748
|
MCPSecurityProxy,
|
|
2136
2749
|
OutputFirewall,
|
|
2750
|
+
PowerBIExporter,
|
|
2751
|
+
PolicyLearningLoop,
|
|
2137
2752
|
RetrievalSanitizer,
|
|
2138
2753
|
SessionBuffer,
|
|
2754
|
+
ShadowConsensusAuditor,
|
|
2139
2755
|
TokenBudgetFirewall,
|
|
2140
2756
|
ToolPermissionFirewall,
|
|
2757
|
+
ValueAtRiskCircuitBreaker,
|
|
2141
2758
|
VisualInstructionDetector,
|
|
2142
2759
|
SENSITIVE_PATTERNS,
|
|
2143
2760
|
PROMPT_INJECTION_RULES,
|
|
@@ -2167,6 +2784,19 @@ module.exports = {
|
|
|
2167
2784
|
runRedTeamSuite,
|
|
2168
2785
|
buildShieldOptions,
|
|
2169
2786
|
summarizeOperationalTelemetry,
|
|
2787
|
+
suggestPolicyOverride,
|
|
2788
|
+
normalizeIdentityMetadata,
|
|
2789
|
+
buildEnterpriseTelemetryEvent,
|
|
2790
|
+
buildPowerBIRecord,
|
|
2791
|
+
buildComplianceEventBundle,
|
|
2792
|
+
sanitizeAuditEvent,
|
|
2793
|
+
detectOperationalDrift,
|
|
2794
|
+
DataClassificationGate,
|
|
2795
|
+
ProviderRoutingPolicy,
|
|
2796
|
+
ApprovalInboxModel,
|
|
2797
|
+
RetrievalTrustScorer,
|
|
2798
|
+
OutboundCommunicationGuard,
|
|
2799
|
+
UploadQuarantineWorkflow,
|
|
2170
2800
|
parseJsonOutput,
|
|
2171
2801
|
createOpenAIAdapter,
|
|
2172
2802
|
createAnthropicAdapter,
|