@drakon-systems/shieldcortex-realtime 4.32.1 → 4.32.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/cloud-sync.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  // plugins/openclaw/cloud-sync.ts
2
2
  //
3
- // Network egress for SC threat events. See CHANGELOG.md v4.12.8 / v4.12.9.
3
+ // Network egress for SC realtime input-scan threat events. See CHANGELOG.md
4
+ // v4.12.8 / v4.12.9. Posts canonical audit entries to /v1/audit/ingest — the
5
+ // only ingest route the SaaS exposes (the old /v1/threats route never existed
6
+ // server-side, so these events were silently 404'd and dropped).
7
+
8
+ import { toAuditEntry } from './audit-entry.js';
4
9
 
5
10
  type CloudSyncConfig = {
6
11
  cloudEnabled?: boolean;
@@ -15,20 +20,29 @@ export function cloudSync(threat: Record<string, unknown>, cfg: CloudSyncConfig)
15
20
  // cloud sync was "off" (ClawScan finding: external scanning without a local-only
16
21
  // default).
17
22
  if (!cfg.cloudEnabled || !cfg.cloudApiKey) return;
18
- // Privacy: never transmit raw LLM input. Strip any input content/preview snippet
19
- // forward threat METADATA only (ClawScan finding: previews may contain credentials
20
- // or confidential data). The local audit log keeps fuller detail for triage.
21
- const metadata: Record<string, unknown> = { ...threat };
22
- delete metadata.content;
23
- delete metadata.preview;
24
- const url = `${cfg.cloudBaseUrl || 'https://api.shieldcortex.ai'}/v1/threats`;
23
+ // Privacy: never transmit raw LLM input. We build a fresh canonical entry from
24
+ // named METADATA fields only content/preview are never read or copied, so they
25
+ // cannot leak even though the caller's `threat` object carries them (ClawScan
26
+ // finding: previews may contain credentials or confidential data). The explicit
27
+ // field-passing below IS the privacy boundary. The local audit log keeps fuller
28
+ // detail for triage.
29
+ const entry = toAuditEntry({
30
+ kind: 'realtime',
31
+ hook: typeof threat.hook === 'string' ? threat.hook : undefined,
32
+ sessionId: typeof threat.sessionId === 'string' ? threat.sessionId : undefined,
33
+ model: typeof threat.model === 'string' ? threat.model : undefined,
34
+ reason: typeof threat.reason === 'string' ? threat.reason : undefined,
35
+ ts: typeof threat.ts === 'string' ? threat.ts : undefined,
36
+ });
37
+ if (!entry) return;
38
+ const url = `${cfg.cloudBaseUrl || 'https://api.shieldcortex.ai'}/v1/audit/ingest`;
25
39
  fetch(url, {
26
40
  method: 'POST',
27
41
  headers: {
28
42
  'Content-Type': 'application/json',
29
43
  Authorization: `Bearer ${cfg.cloudApiKey}`,
30
44
  },
31
- body: JSON.stringify(metadata),
45
+ body: JSON.stringify({ entries: [entry] }),
32
46
  signal: AbortSignal.timeout(5000),
33
47
  }).catch(() => {
34
48
  // Fire-and-forget — never block on cloud sync failure
@@ -0,0 +1,56 @@
1
+ // plugins/openclaw/audit-entry.ts
2
+ // Canonical audit-entry builder for the OpenClaw plugin's cloud egress.
3
+ // Mirrors the SaaS ingestSchema (ShieldCortex-internal/src/routes/v1/audit.ts).
4
+ // Privacy: callers pass METADATA only — this never reads content/preview.
5
+ function clamp01(n) {
6
+ const v = typeof n === 'number' && Number.isFinite(n) ? n : 0;
7
+ return Math.max(0, Math.min(1, v));
8
+ }
9
+ function normalizeFirewallResult(raw, fallback) {
10
+ const s = String(raw ?? '').toUpperCase();
11
+ if (s === 'ALLOW' || s === 'BLOCK' || s === 'QUARANTINE')
12
+ return s;
13
+ if (s === 'DENY' || s === 'DENIED' || s === 'AUTO_DENIED' || s === 'BLOCKED')
14
+ return 'BLOCK';
15
+ return fallback;
16
+ }
17
+ function isoTimestamp(raw) {
18
+ const c = typeof raw === 'string' ? raw : '';
19
+ const t = c ? new Date(c).getTime() : NaN;
20
+ return Number.isNaN(t) ? new Date().toISOString() : new Date(t).toISOString();
21
+ }
22
+ export function toAuditEntry(input) {
23
+ if (!input)
24
+ return null;
25
+ const timestamp = isoTimestamp(input.ts);
26
+ if (input.kind === 'intercept') {
27
+ const anomaly = clamp01(input.anomalyScore);
28
+ return {
29
+ source_type: 'openclaw-interceptor',
30
+ source_identifier: input.tool || 'openclaw',
31
+ trust_score: input.trustScore != null ? clamp01(input.trustScore) : clamp01(1 - anomaly),
32
+ sensitivity_level: input.sensitivityLevel || 'INTERNAL',
33
+ firewall_result: normalizeFirewallResult(input.firewallResult, 'QUARANTINE'),
34
+ anomaly_score: anomaly,
35
+ threat_indicators: Array.isArray(input.threats) ? input.threats.map(String) : [],
36
+ fragmentation_score: input.fragmentationScore == null ? null : clamp01(input.fragmentationScore),
37
+ reason: `OpenClaw intercept: ${input.tool ?? 'tool'} → ${input.outcome ?? input.action ?? 'logged'}`,
38
+ pipeline_duration_ms: Math.max(0, Math.trunc(input.pipelineDurationMs ?? 0)),
39
+ timestamp,
40
+ };
41
+ }
42
+ const reason = input.reason || 'OpenClaw realtime threat';
43
+ return {
44
+ source_type: input.hook || 'openclaw-realtime',
45
+ source_identifier: input.model || input.sessionId || 'openclaw',
46
+ trust_score: 0.5,
47
+ sensitivity_level: 'INTERNAL',
48
+ firewall_result: 'QUARANTINE',
49
+ anomaly_score: 0,
50
+ threat_indicators: [reason],
51
+ fragmentation_score: null,
52
+ reason,
53
+ pipeline_duration_ms: 0,
54
+ timestamp,
55
+ };
56
+ }
@@ -1,6 +1,10 @@
1
1
  // plugins/openclaw/cloud-sync.ts
2
2
  //
3
- // Network egress for SC threat events. See CHANGELOG.md v4.12.8 / v4.12.9.
3
+ // Network egress for SC realtime input-scan threat events. See CHANGELOG.md
4
+ // v4.12.8 / v4.12.9. Posts canonical audit entries to /v1/audit/ingest — the
5
+ // only ingest route the SaaS exposes (the old /v1/threats route never existed
6
+ // server-side, so these events were silently 404'd and dropped).
7
+ import { toAuditEntry } from './audit-entry.js';
4
8
  export function cloudSync(threat, cfg) {
5
9
  // Consent gate: require cloud explicitly enabled AND an API key — matching every
6
10
  // other egress sender (intercept-ingest.ts, src/cloud/*). Previously this checked
@@ -9,20 +13,30 @@ export function cloudSync(threat, cfg) {
9
13
  // default).
10
14
  if (!cfg.cloudEnabled || !cfg.cloudApiKey)
11
15
  return;
12
- // Privacy: never transmit raw LLM input. Strip any input content/preview snippet
13
- // forward threat METADATA only (ClawScan finding: previews may contain credentials
14
- // or confidential data). The local audit log keeps fuller detail for triage.
15
- const metadata = { ...threat };
16
- delete metadata.content;
17
- delete metadata.preview;
18
- const url = `${cfg.cloudBaseUrl || 'https://api.shieldcortex.ai'}/v1/threats`;
16
+ // Privacy: never transmit raw LLM input. We build a fresh canonical entry from
17
+ // named METADATA fields only content/preview are never read or copied, so they
18
+ // cannot leak even though the caller's `threat` object carries them (ClawScan
19
+ // finding: previews may contain credentials or confidential data). The explicit
20
+ // field-passing below IS the privacy boundary. The local audit log keeps fuller
21
+ // detail for triage.
22
+ const entry = toAuditEntry({
23
+ kind: 'realtime',
24
+ hook: typeof threat.hook === 'string' ? threat.hook : undefined,
25
+ sessionId: typeof threat.sessionId === 'string' ? threat.sessionId : undefined,
26
+ model: typeof threat.model === 'string' ? threat.model : undefined,
27
+ reason: typeof threat.reason === 'string' ? threat.reason : undefined,
28
+ ts: typeof threat.ts === 'string' ? threat.ts : undefined,
29
+ });
30
+ if (!entry)
31
+ return;
32
+ const url = `${cfg.cloudBaseUrl || 'https://api.shieldcortex.ai'}/v1/audit/ingest`;
19
33
  fetch(url, {
20
34
  method: 'POST',
21
35
  headers: {
22
36
  'Content-Type': 'application/json',
23
37
  Authorization: `Bearer ${cfg.cloudApiKey}`,
24
38
  },
25
- body: JSON.stringify(metadata),
39
+ body: JSON.stringify({ entries: [entry] }),
26
40
  signal: AbortSignal.timeout(5000),
27
41
  }).catch(() => {
28
42
  // Fire-and-forget — never block on cloud sync failure
package/dist/index.js CHANGED
@@ -562,8 +562,9 @@ export async function scanLlmInput(event, _ctx) {
562
562
  };
563
563
  auditLog(entry);
564
564
  loadConfig()
565
- // Pass the local entry as-is; cloudSync strips the input preview/content
566
- // before transmit (metadata-only egress). No raw LLM input leaves here.
565
+ // Pass the local entry as-is; cloudSync rebuilds a canonical metadata-only
566
+ // entry from named fields and never reads preview/content. No raw LLM input
567
+ // leaves here.
567
568
  .then(cfg => cloudSync(entry, cfg))
568
569
  .catch(() => { });
569
570
  }
@@ -1,21 +1,38 @@
1
+ // plugins/openclaw/intercept-ingest.ts
2
+ import { toAuditEntry } from './audit-entry.js';
1
3
  export function syncInterceptEvent(event, config) {
2
4
  if (!config.cloudEnabled || !config.cloudApiKey)
3
5
  return;
4
- const url = `${config.cloudBaseUrl}/v1/audit/ingest`;
5
- // Privacy: forward audit METADATA only strip the content preview so raw memory
6
- // text never leaves the machine (ClawScan finding: previews may contain credentials
7
- // or confidential data). The local audit JSONL retains the preview for triage.
8
- const metadata = { ...event };
9
- delete metadata.preview;
10
- fetch(url, {
6
+ // Privacy: build a canonical audit entry from METADATA only — the content
7
+ // preview never leaves the machine (ClawScan finding: previews may contain
8
+ // credentials or confidential data). The local audit JSONL retains the
9
+ // preview for triage. toAuditEntry has no notion of preview/content.
10
+ const entry = toAuditEntry({
11
+ kind: 'intercept',
12
+ tool: event.tool,
13
+ firewallResult: event.firewallResult,
14
+ threats: event.threats,
15
+ anomalyScore: event.anomalyScore,
16
+ trustScore: event.trustScore,
17
+ sensitivityLevel: event.sensitivityLevel,
18
+ fragmentationScore: event.fragmentationScore,
19
+ outcome: event.outcome,
20
+ action: event.action,
21
+ pipelineDurationMs: event.pipelineDurationMs,
22
+ ts: event.ts,
23
+ });
24
+ if (!entry)
25
+ return;
26
+ // SaaS /v1/audit/ingest requires { entries: [<canonical snake_case entry>] }
27
+ // (zod ingestSchema). The old { events: [...] } shape was rejected 400 and
28
+ // every interceptor POST was silently dropped.
29
+ fetch(`${config.cloudBaseUrl}/v1/audit/ingest`, {
11
30
  method: 'POST',
12
31
  headers: {
13
32
  'Content-Type': 'application/json',
14
33
  Authorization: `Bearer ${config.cloudApiKey}`,
15
34
  },
16
- body: JSON.stringify({
17
- events: [{ ...metadata, source: 'openclaw-interceptor' }],
18
- }),
35
+ body: JSON.stringify({ entries: [entry] }),
19
36
  signal: AbortSignal.timeout(5_000),
20
37
  }).catch(() => {
21
38
  // Fire-and-forget — never block on cloud sync failure
@@ -211,7 +211,10 @@ export function createInterceptor(config, pipeline, options) {
211
211
  const xrayEntry = {
212
212
  type: 'intercept', tool: context.toolName, severity: 'critical',
213
213
  firewallResult: 'BLOCK', threats: xrayResult.findings.map(f => f.category),
214
- anomalyScore: 1, action: 'auto_deny', outcome: 'auto_denied',
214
+ anomalyScore: 1,
215
+ // X-Ray short-circuits before the pipeline runs — no pipeline result.
216
+ trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
217
+ action: 'auto_deny', outcome: 'auto_denied',
215
218
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
216
219
  };
217
220
  emitAudit(xrayEntry);
@@ -221,12 +224,21 @@ export function createInterceptor(config, pipeline, options) {
221
224
  let firewallResult;
222
225
  let threats;
223
226
  let anomalyScore;
227
+ let trustScore;
228
+ let sensitivityLevel;
229
+ let fragmentationScore;
230
+ let pipelineDurationMs;
224
231
  try {
232
+ const pipelineStart = Date.now();
225
233
  const result = pipeline(content, title, { type: 'agent', identifier: 'openclaw' });
234
+ pipelineDurationMs = Date.now() - pipelineStart;
226
235
  severity = mapSeverity(result.firewall);
227
236
  firewallResult = result.firewall.result;
228
237
  threats = result.firewall.threatIndicators;
229
238
  anomalyScore = result.firewall.anomalyScore;
239
+ trustScore = result.trust.score;
240
+ sensitivityLevel = result.sensitivity.level;
241
+ fragmentationScore = result.fragmentation?.score ?? null;
230
242
  }
231
243
  catch (err) {
232
244
  log.warn(`[shieldcortex] ⚠️ Defence pipeline error: ${err instanceof Error ? err.message : err}`);
@@ -234,6 +246,8 @@ export function createInterceptor(config, pipeline, options) {
234
246
  const entry = {
235
247
  type: 'intercept', tool: context.toolName, severity: 'high',
236
248
  firewallResult: 'ERROR', threats: ['pipeline_error'], anomalyScore: 0,
249
+ // Pipeline threw — no result in scope, use documented defaults.
250
+ trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
237
251
  action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
238
252
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
239
253
  };
@@ -246,7 +260,8 @@ export function createInterceptor(config, pipeline, options) {
246
260
  if (denyCache.isDenied(context.toolName, fullContent)) {
247
261
  const entry = {
248
262
  type: 'intercept', tool: context.toolName, severity, firewallResult,
249
- threats, anomalyScore, action: 'auto_deny', outcome: 'auto_denied',
263
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
264
+ action: 'auto_deny', outcome: 'auto_denied',
250
265
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
251
266
  };
252
267
  emitAudit(entry);
@@ -256,7 +271,8 @@ export function createInterceptor(config, pipeline, options) {
256
271
  if (action === 'log') {
257
272
  const entry = {
258
273
  type: 'intercept', tool: context.toolName, severity, firewallResult,
259
- threats, anomalyScore, action: 'log', outcome: 'logged',
274
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
275
+ action: 'log', outcome: 'logged',
260
276
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
261
277
  };
262
278
  emitAudit(entry);
@@ -266,7 +282,8 @@ export function createInterceptor(config, pipeline, options) {
266
282
  log.warn(`[shieldcortex] ⚠️ ${severity} risk in ${context.toolName}: ${threats.join(', ') || 'anomaly detected'}`);
267
283
  const entry = {
268
284
  type: 'intercept', tool: context.toolName, severity, firewallResult,
269
- threats, anomalyScore, action: 'warn', outcome: 'warned',
285
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
286
+ action: 'warn', outcome: 'warned',
270
287
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
271
288
  };
272
289
  emitAudit(entry);
@@ -279,7 +296,8 @@ export function createInterceptor(config, pipeline, options) {
279
296
  log.warn(`[shieldcortex] ⚠️ requireApproval not available for ${severity} risk in ${context.toolName} — failure policy: ${failAction}`);
280
297
  const entry = {
281
298
  type: 'intercept', tool: context.toolName, severity, firewallResult,
282
- threats, anomalyScore, action: 'require_approval',
299
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
300
+ action: 'require_approval',
283
301
  outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
284
302
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
285
303
  };
@@ -293,7 +311,8 @@ export function createInterceptor(config, pipeline, options) {
293
311
  log.warn('[shieldcortex] ⚠️ Too many approval prompts — auto-denying');
294
312
  const entry = {
295
313
  type: 'intercept', tool: context.toolName, severity, firewallResult,
296
- threats, anomalyScore, action: 'rate_limit', outcome: 'auto_denied',
314
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
315
+ action: 'rate_limit', outcome: 'auto_denied',
297
316
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
298
317
  };
299
318
  emitAudit(entry);
@@ -310,7 +329,8 @@ export function createInterceptor(config, pipeline, options) {
310
329
  log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
311
330
  const entry = {
312
331
  type: 'intercept', tool: context.toolName, severity, firewallResult,
313
- threats, anomalyScore, action: 'require_approval',
332
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
333
+ action: 'require_approval',
314
334
  outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
315
335
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
316
336
  };
@@ -323,7 +343,8 @@ export function createInterceptor(config, pipeline, options) {
323
343
  if (approved) {
324
344
  const entry = {
325
345
  type: 'intercept', tool: context.toolName, severity, firewallResult,
326
- threats, anomalyScore, action: 'require_approval', outcome: 'approved',
346
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
347
+ action: 'require_approval', outcome: 'approved',
327
348
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
328
349
  };
329
350
  emitAudit(entry);
@@ -333,7 +354,8 @@ export function createInterceptor(config, pipeline, options) {
333
354
  denyCache.addDenial(context.toolName, fullContent);
334
355
  const entry = {
335
356
  type: 'intercept', tool: context.toolName, severity, firewallResult,
336
- threats, anomalyScore, action: 'require_approval', outcome: 'denied',
357
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
358
+ action: 'require_approval', outcome: 'denied',
337
359
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
338
360
  };
339
361
  emitAudit(entry);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.32.1",
3
+ "version": "4.32.3",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
package/index.ts CHANGED
@@ -680,8 +680,9 @@ export async function scanLlmInput(event: LlmInputEvent, _ctx: AgentCtx): Promis
680
680
  };
681
681
  auditLog(entry);
682
682
  loadConfig()
683
- // Pass the local entry as-is; cloudSync strips the input preview/content
684
- // before transmit (metadata-only egress). No raw LLM input leaves here.
683
+ // Pass the local entry as-is; cloudSync rebuilds a canonical metadata-only
684
+ // entry from named fields and never reads preview/content. No raw LLM input
685
+ // leaves here.
685
686
  .then(cfg => cloudSync(entry, cfg))
686
687
  .catch(() => {});
687
688
  }
@@ -1,4 +1,5 @@
1
1
  // plugins/openclaw/intercept-ingest.ts
2
+ import { toAuditEntry } from './audit-entry.js';
2
3
  import type { InterceptAuditEntry } from './interceptor.js';
3
4
 
4
5
  interface CloudConfig {
@@ -10,23 +11,36 @@ interface CloudConfig {
10
11
  export function syncInterceptEvent(event: InterceptAuditEntry, config: CloudConfig): void {
11
12
  if (!config.cloudEnabled || !config.cloudApiKey) return;
12
13
 
13
- const url = `${config.cloudBaseUrl}/v1/audit/ingest`;
14
-
15
- // Privacy: forward audit METADATA only strip the content preview so raw memory
16
- // text never leaves the machine (ClawScan finding: previews may contain credentials
17
- // or confidential data). The local audit JSONL retains the preview for triage.
18
- const metadata: Record<string, unknown> = { ...event };
19
- delete metadata.preview;
14
+ // Privacy: build a canonical audit entry from METADATA only — the content
15
+ // preview never leaves the machine (ClawScan finding: previews may contain
16
+ // credentials or confidential data). The local audit JSONL retains the
17
+ // preview for triage. toAuditEntry has no notion of preview/content.
18
+ const entry = toAuditEntry({
19
+ kind: 'intercept',
20
+ tool: event.tool,
21
+ firewallResult: event.firewallResult,
22
+ threats: event.threats,
23
+ anomalyScore: event.anomalyScore,
24
+ trustScore: event.trustScore,
25
+ sensitivityLevel: event.sensitivityLevel,
26
+ fragmentationScore: event.fragmentationScore,
27
+ outcome: event.outcome,
28
+ action: event.action,
29
+ pipelineDurationMs: event.pipelineDurationMs,
30
+ ts: event.ts,
31
+ });
32
+ if (!entry) return;
20
33
 
21
- fetch(url, {
34
+ // SaaS /v1/audit/ingest requires { entries: [<canonical snake_case entry>] }
35
+ // (zod ingestSchema). The old { events: [...] } shape was rejected 400 and
36
+ // every interceptor POST was silently dropped.
37
+ fetch(`${config.cloudBaseUrl}/v1/audit/ingest`, {
22
38
  method: 'POST',
23
39
  headers: {
24
40
  'Content-Type': 'application/json',
25
41
  Authorization: `Bearer ${config.cloudApiKey}`,
26
42
  },
27
- body: JSON.stringify({
28
- events: [{ ...metadata, source: 'openclaw-interceptor' }],
29
- }),
43
+ body: JSON.stringify({ entries: [entry] }),
30
44
  signal: AbortSignal.timeout(5_000),
31
45
  }).catch(() => {
32
46
  // Fire-and-forget — never block on cloud sync failure
package/interceptor.ts CHANGED
@@ -27,6 +27,10 @@ export interface InterceptAuditEntry {
27
27
  firewallResult: string;
28
28
  threats: string[];
29
29
  anomalyScore: number;
30
+ trustScore: number; // from the pipeline result's trust score
31
+ sensitivityLevel: string; // from the pipeline result's sensitivity level
32
+ fragmentationScore: number | null; // from the pipeline result's fragmentation score, or null
33
+ pipelineDurationMs: number; // wall-clock ms around the runDefencePipeline call
30
34
  action: InterceptAction | 'auto_deny' | 'rate_limit';
31
35
  outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied';
32
36
  preview: string;
@@ -271,6 +275,10 @@ function xrayMemoryGuard(content: string, title?: string): XRayGuardResult {
271
275
 
272
276
  // --- Interceptor Factory ---
273
277
 
278
+ // Subset of shieldcortex/defence DefencePipelineResult (src/defence/pipeline.ts).
279
+ // Field paths verified against src/defence/types.ts: trust.score (TrustScore.score),
280
+ // sensitivity.level (SensitivityClassification.level), fragmentation.score
281
+ // (FragmentationAnalysis.score; fragmentation itself is nullable).
274
282
  type PipelineRunner = (content: string, title: string, source: { type: string; identifier: string }) => {
275
283
  allowed: boolean;
276
284
  firewall: {
@@ -280,6 +288,9 @@ type PipelineRunner = (content: string, title: string, source: { type: string; i
280
288
  anomalyScore: number;
281
289
  blockedPatterns: string[];
282
290
  };
291
+ trust: { score: number };
292
+ sensitivity: { level: string };
293
+ fragmentation: { score: number } | null;
283
294
  auditId: number;
284
295
  };
285
296
 
@@ -319,7 +330,10 @@ export function createInterceptor(
319
330
  const xrayEntry: InterceptAuditEntry = {
320
331
  type: 'intercept', tool: context.toolName, severity: 'critical',
321
332
  firewallResult: 'BLOCK', threats: xrayResult.findings.map(f => f.category),
322
- anomalyScore: 1, action: 'auto_deny', outcome: 'auto_denied',
333
+ anomalyScore: 1,
334
+ // X-Ray short-circuits before the pipeline runs — no pipeline result.
335
+ trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
336
+ action: 'auto_deny', outcome: 'auto_denied',
323
337
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
324
338
  };
325
339
  emitAudit(xrayEntry);
@@ -330,19 +344,30 @@ export function createInterceptor(
330
344
  let firewallResult: string;
331
345
  let threats: string[];
332
346
  let anomalyScore: number;
347
+ let trustScore: number;
348
+ let sensitivityLevel: string;
349
+ let fragmentationScore: number | null;
350
+ let pipelineDurationMs: number;
333
351
 
334
352
  try {
353
+ const pipelineStart = Date.now();
335
354
  const result = pipeline(content, title, { type: 'agent', identifier: 'openclaw' });
355
+ pipelineDurationMs = Date.now() - pipelineStart;
336
356
  severity = mapSeverity(result.firewall);
337
357
  firewallResult = result.firewall.result;
338
358
  threats = result.firewall.threatIndicators;
339
359
  anomalyScore = result.firewall.anomalyScore;
360
+ trustScore = result.trust.score;
361
+ sensitivityLevel = result.sensitivity.level;
362
+ fragmentationScore = result.fragmentation?.score ?? null;
340
363
  } catch (err) {
341
364
  log.warn(`[shieldcortex] ⚠️ Defence pipeline error: ${err instanceof Error ? err.message : err}`);
342
365
  const failAction = config.failurePolicy.high;
343
366
  const entry: InterceptAuditEntry = {
344
367
  type: 'intercept', tool: context.toolName, severity: 'high',
345
368
  firewallResult: 'ERROR', threats: ['pipeline_error'], anomalyScore: 0,
369
+ // Pipeline threw — no result in scope, use documented defaults.
370
+ trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
346
371
  action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
347
372
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
348
373
  };
@@ -356,7 +381,8 @@ export function createInterceptor(
356
381
  if (denyCache.isDenied(context.toolName, fullContent)) {
357
382
  const entry: InterceptAuditEntry = {
358
383
  type: 'intercept', tool: context.toolName, severity, firewallResult,
359
- threats, anomalyScore, action: 'auto_deny', outcome: 'auto_denied',
384
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
385
+ action: 'auto_deny', outcome: 'auto_denied',
360
386
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
361
387
  };
362
388
  emitAudit(entry);
@@ -368,7 +394,8 @@ export function createInterceptor(
368
394
  if (action === 'log') {
369
395
  const entry: InterceptAuditEntry = {
370
396
  type: 'intercept', tool: context.toolName, severity, firewallResult,
371
- threats, anomalyScore, action: 'log', outcome: 'logged',
397
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
398
+ action: 'log', outcome: 'logged',
372
399
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
373
400
  };
374
401
  emitAudit(entry);
@@ -379,7 +406,8 @@ export function createInterceptor(
379
406
  log.warn(`[shieldcortex] ⚠️ ${severity} risk in ${context.toolName}: ${threats.join(', ') || 'anomaly detected'}`);
380
407
  const entry: InterceptAuditEntry = {
381
408
  type: 'intercept', tool: context.toolName, severity, firewallResult,
382
- threats, anomalyScore, action: 'warn', outcome: 'warned',
409
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
410
+ action: 'warn', outcome: 'warned',
383
411
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
384
412
  };
385
413
  emitAudit(entry);
@@ -393,7 +421,8 @@ export function createInterceptor(
393
421
  log.warn(`[shieldcortex] ⚠️ requireApproval not available for ${severity} risk in ${context.toolName} — failure policy: ${failAction}`);
394
422
  const entry: InterceptAuditEntry = {
395
423
  type: 'intercept', tool: context.toolName, severity, firewallResult,
396
- threats, anomalyScore, action: 'require_approval',
424
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
425
+ action: 'require_approval',
397
426
  outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
398
427
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
399
428
  };
@@ -408,7 +437,8 @@ export function createInterceptor(
408
437
  log.warn('[shieldcortex] ⚠️ Too many approval prompts — auto-denying');
409
438
  const entry: InterceptAuditEntry = {
410
439
  type: 'intercept', tool: context.toolName, severity, firewallResult,
411
- threats, anomalyScore, action: 'rate_limit', outcome: 'auto_denied',
440
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
441
+ action: 'rate_limit', outcome: 'auto_denied',
412
442
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
413
443
  };
414
444
  emitAudit(entry);
@@ -426,7 +456,8 @@ export function createInterceptor(
426
456
  log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
427
457
  const entry: InterceptAuditEntry = {
428
458
  type: 'intercept', tool: context.toolName, severity, firewallResult,
429
- threats, anomalyScore, action: 'require_approval',
459
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
460
+ action: 'require_approval',
430
461
  outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
431
462
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
432
463
  };
@@ -440,7 +471,8 @@ export function createInterceptor(
440
471
  if (approved) {
441
472
  const entry: InterceptAuditEntry = {
442
473
  type: 'intercept', tool: context.toolName, severity, firewallResult,
443
- threats, anomalyScore, action: 'require_approval', outcome: 'approved',
474
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
475
+ action: 'require_approval', outcome: 'approved',
444
476
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
445
477
  };
446
478
  emitAudit(entry);
@@ -451,7 +483,8 @@ export function createInterceptor(
451
483
  denyCache.addDenial(context.toolName, fullContent);
452
484
  const entry: InterceptAuditEntry = {
453
485
  type: 'intercept', tool: context.toolName, severity, firewallResult,
454
- threats, anomalyScore, action: 'require_approval', outcome: 'denied',
486
+ threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
487
+ action: 'require_approval', outcome: 'denied',
455
488
  preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
456
489
  };
457
490
  emitAudit(entry);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.32.1",
3
+ "version": "4.32.3",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.32.1",
3
+ "version": "4.32.3",
4
4
  "description": "OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory extraction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",