@drakon-systems/shieldcortex-realtime 4.47.7 → 4.47.8

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
@@ -113,6 +113,7 @@ Supported plugin config keys:
113
113
  - `actionGuard.enabled`: turn the before-tool-call Action Guard on or off (default `true`)
114
114
  - `actionGuard.enforce`: enforce dangerous-operation gating (default `true`); `false` opts down to warn-and-allow. Catastrophic operations are blocked regardless.
115
115
  - `actionGuard.autoApprove`: array of operation allowlist entries for unattended agents that legitimately need specific dangerous operations
116
+ - `actionGuard.auditAllows`: audit recognised (sensitive-tier) allow-decisions so "scanned & allowed" is distinguishable from "never scanned" (default `true`; benign allows are never audited)
116
117
  - `failurePolicy`: per-severity verdict when a decision can't be obtained unattended (defaults: `low`/`medium` allow, `high`/`critical` deny)
117
118
 
118
119
  ## Auto-memory
@@ -35,6 +35,7 @@ const DEFAULT_CONFIG = {
35
35
  enabled: true,
36
36
  enforce: true,
37
37
  autoApprove: [],
38
+ auditAllows: true,
38
39
  },
39
40
  };
40
41
  export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
@@ -220,6 +221,20 @@ export function formatActionGuardPrompt(toolName, v) {
220
221
  }
221
222
  // --- Audit Logging (local JSONL) ---
222
223
  const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
224
+ // Issue #95: an unwritable audit sink used to be swallowed by this bare catch —
225
+ // entries silently dropped forever. Still best-effort (an audit failure must
226
+ // never block the agent), but the FIRST failure now warns loudly with the sink
227
+ // path and the error, and later failures keep a drop count for the breadcrumb.
228
+ let auditSinkFailures = 0;
229
+ export function noteAuditSinkFailure(err) {
230
+ auditSinkFailures++;
231
+ if (auditSinkFailures === 1) {
232
+ const detail = err instanceof Error ? err.message : String(err);
233
+ console.warn(`[shieldcortex] ⚠️ audit sink UNWRITABLE (${AUDIT_DIR}): ${detail} — audit entries are being DROPPED. ` +
234
+ `Fix the directory permissions/disk; enforcement continues but leaves no trail until this is resolved.`);
235
+ }
236
+ }
237
+ export function __resetAuditSinkFailuresForTest() { auditSinkFailures = 0; }
223
238
  function writeAuditEntry(entry) {
224
239
  try {
225
240
  mkdirSync(AUDIT_DIR, { recursive: true });
@@ -227,8 +242,9 @@ function writeAuditEntry(entry) {
227
242
  const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
228
243
  appendFileSync(file, JSON.stringify(entry) + '\n');
229
244
  }
230
- catch {
231
- // Best-effort — never block on audit failure
245
+ catch (err) {
246
+ // Best-effort — never block on audit failure, but never silent either (#95).
247
+ noteAuditSinkFailure(err);
232
248
  }
233
249
  }
234
250
  // --- X-Ray Inline Guard ---
@@ -288,9 +304,9 @@ export function createInterceptor(config, pipeline, options) {
288
304
  function guardAuditBase(toolName, v, preview) {
289
305
  return {
290
306
  type: 'intercept', tool: toolName,
291
- severity: v.severity === 'catastrophic' ? 'critical' : 'high',
307
+ severity: v.severity === 'catastrophic' ? 'critical' : v.decision === 'allow' ? 'low' : 'high',
292
308
  firewallResult: 'ACTION_GUARD', threats: v.signals,
293
- anomalyScore: v.decision === 'block' ? 1 : 0.6,
309
+ anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
294
310
  trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
295
311
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
296
312
  };
@@ -334,8 +350,18 @@ export function createInterceptor(config, pipeline, options) {
334
350
  handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
335
351
  return;
336
352
  }
337
- if (v.decision === 'allow')
353
+ if (v.decision === 'allow') {
354
+ // Issue #95: a RECOGNISED allow (the guard evaluated a known operation
355
+ // family and let it through — severity above benign) leaves an audit
356
+ // entry, so forensics can distinguish "scanned & allowed" from "never
357
+ // scanned". Benign allows stay unaudited by design (volume discipline);
358
+ // `actionGuard.auditAllows: false` opts the recognised entries off too.
359
+ if (v.severity !== 'benign' && actionGuardCfg.auditAllows !== false) {
360
+ const allowPreview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
361
+ emitAudit({ ...guardAuditBase(context.toolName, v, allowPreview), action: 'allow', outcome: 'allowed' });
362
+ }
338
363
  return;
364
+ }
339
365
  const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
340
366
  const base = guardAuditBase(context.toolName, v, preview);
341
367
  const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.7",
3
+ "version": "4.47.8",
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,
@@ -93,6 +93,12 @@
93
93
  "description": "Family/action/signal names pre-approved for unattended agents that legitimately need specific dangerous operations (case-insensitive substring match). Never applies to catastrophic operations.",
94
94
  "type": "array",
95
95
  "advanced": true
96
+ },
97
+ "interceptor.actionGuard.auditAllows": {
98
+ "label": "Audit Recognised Allows",
99
+ "description": "Write an audit entry when the guard evaluates a recognised (sensitive-tier) operation and allows it, so forensics can distinguish scanned-and-allowed from never-scanned. Benign allows are never audited.",
100
+ "type": "boolean",
101
+ "advanced": true
96
102
  }
97
103
  },
98
104
  "configSchema": {
@@ -232,6 +238,10 @@
232
238
  "type": "string"
233
239
  },
234
240
  "default": []
241
+ },
242
+ "auditAllows": {
243
+ "type": "boolean",
244
+ "default": true
235
245
  }
236
246
  }
237
247
  }
package/interceptor.ts CHANGED
@@ -26,6 +26,9 @@ export interface ActionGuardConfig {
26
26
  enabled: boolean;
27
27
  enforce: boolean;
28
28
  autoApprove?: string[];
29
+ /** Audit recognised (severity 'sensitive'+) allow-decisions. Default true (issue #95).
30
+ * Benign allows are never audited — on a busy agent every `ls` would drown the stream. */
31
+ auditAllows?: boolean;
29
32
  }
30
33
 
31
34
  /** Structural shape of a Tool Action Guard verdict (kept local to avoid a
@@ -66,8 +69,8 @@ export interface InterceptAuditEntry {
66
69
  sensitivityLevel: string; // from the pipeline result's sensitivity level
67
70
  fragmentationScore: number | null; // from the pipeline result's fragmentation score, or null
68
71
  pipelineDurationMs: number; // wall-clock ms around the runDefencePipeline call
69
- action: InterceptAction | 'auto_deny' | 'rate_limit';
70
- outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied';
72
+ action: InterceptAction | 'auto_deny' | 'rate_limit' | 'allow';
73
+ outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied' | 'allowed';
71
74
  preview: string;
72
75
  ts: string;
73
76
  }
@@ -107,6 +110,7 @@ const DEFAULT_CONFIG: InterceptorConfig = {
107
110
  enabled: true,
108
111
  enforce: true,
109
112
  autoApprove: [],
113
+ auditAllows: true,
110
114
  },
111
115
  };
112
116
 
@@ -327,14 +331,32 @@ export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLik
327
331
 
328
332
  const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
329
333
 
334
+ // Issue #95: an unwritable audit sink used to be swallowed by this bare catch —
335
+ // entries silently dropped forever. Still best-effort (an audit failure must
336
+ // never block the agent), but the FIRST failure now warns loudly with the sink
337
+ // path and the error, and later failures keep a drop count for the breadcrumb.
338
+ let auditSinkFailures = 0;
339
+ export function noteAuditSinkFailure(err: unknown): void {
340
+ auditSinkFailures++;
341
+ if (auditSinkFailures === 1) {
342
+ const detail = err instanceof Error ? err.message : String(err);
343
+ console.warn(
344
+ `[shieldcortex] ⚠️ audit sink UNWRITABLE (${AUDIT_DIR}): ${detail} — audit entries are being DROPPED. ` +
345
+ `Fix the directory permissions/disk; enforcement continues but leaves no trail until this is resolved.`,
346
+ );
347
+ }
348
+ }
349
+ export function __resetAuditSinkFailuresForTest(): void { auditSinkFailures = 0; }
350
+
330
351
  function writeAuditEntry(entry: InterceptAuditEntry): void {
331
352
  try {
332
353
  mkdirSync(AUDIT_DIR, { recursive: true });
333
354
  const date = new Date().toISOString().slice(0, 10);
334
355
  const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
335
356
  appendFileSync(file, JSON.stringify(entry) + '\n');
336
- } catch {
337
- // Best-effort — never block on audit failure
357
+ } catch (err) {
358
+ // Best-effort — never block on audit failure, but never silent either (#95).
359
+ noteAuditSinkFailure(err);
338
360
  }
339
361
  }
340
362
 
@@ -445,9 +467,9 @@ export function createInterceptor(
445
467
  function guardAuditBase(toolName: string, v: ToolGuardVerdictLike, preview: string): Omit<InterceptAuditEntry, 'action' | 'outcome'> {
446
468
  return {
447
469
  type: 'intercept', tool: toolName,
448
- severity: v.severity === 'catastrophic' ? 'critical' : 'high',
470
+ severity: v.severity === 'catastrophic' ? 'critical' : v.decision === 'allow' ? 'low' : 'high',
449
471
  firewallResult: 'ACTION_GUARD', threats: v.signals,
450
- anomalyScore: v.decision === 'block' ? 1 : 0.6,
472
+ anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
451
473
  trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
452
474
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
453
475
  };
@@ -493,7 +515,18 @@ export function createInterceptor(
493
515
  handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
494
516
  return;
495
517
  }
496
- if (v.decision === 'allow') return;
518
+ if (v.decision === 'allow') {
519
+ // Issue #95: a RECOGNISED allow (the guard evaluated a known operation
520
+ // family and let it through — severity above benign) leaves an audit
521
+ // entry, so forensics can distinguish "scanned & allowed" from "never
522
+ // scanned". Benign allows stay unaudited by design (volume discipline);
523
+ // `actionGuard.auditAllows: false` opts the recognised entries off too.
524
+ if (v.severity !== 'benign' && actionGuardCfg.auditAllows !== false) {
525
+ const allowPreview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
526
+ emitAudit({ ...guardAuditBase(context.toolName, v, allowPreview), action: 'allow', outcome: 'allowed' });
527
+ }
528
+ return;
529
+ }
497
530
 
498
531
  const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
499
532
  const base = guardAuditBase(context.toolName, v, preview);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.7",
3
+ "version": "4.47.8",
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,
@@ -93,6 +93,12 @@
93
93
  "description": "Family/action/signal names pre-approved for unattended agents that legitimately need specific dangerous operations (case-insensitive substring match). Never applies to catastrophic operations.",
94
94
  "type": "array",
95
95
  "advanced": true
96
+ },
97
+ "interceptor.actionGuard.auditAllows": {
98
+ "label": "Audit Recognised Allows",
99
+ "description": "Write an audit entry when the guard evaluates a recognised (sensitive-tier) operation and allows it, so forensics can distinguish scanned-and-allowed from never-scanned. Benign allows are never audited.",
100
+ "type": "boolean",
101
+ "advanced": true
96
102
  }
97
103
  },
98
104
  "configSchema": {
@@ -232,6 +238,10 @@
232
238
  "type": "string"
233
239
  },
234
240
  "default": []
241
+ },
242
+ "auditAllows": {
243
+ "type": "boolean",
244
+ "default": true
235
245
  }
236
246
  }
237
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.7",
3
+ "version": "4.47.8",
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",