@drakon-systems/shieldcortex-realtime 4.47.6 → 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 };
@@ -165,6 +166,9 @@ const FALLBACK_CATASTROPHIC_PATTERNS = [
165
166
  // <interp>` admitted (issue #92 must-fix 3) — mirrors tool-action-guard.ts's
166
167
  // pipe-download-to-shell pattern exactly; kept in sync there.
167
168
  /\b(?:curl|wget|fetch)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:env\s+)?(?:\w+=\S*\s+)*(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?!(?:\s+-[a-z]+)*\s+-[cem]\b)/i,
169
+ // Stdin-executing python MODULES defeat the -m exemption above (issue #86.1) —
170
+ // mirrors tool-action-guard.ts's pipe-download-module-exec; kept in sync there.
171
+ /\b(?:curl|wget|fetch)\b[^|\n]*\|[^\n]*\bpython\d?\b[^\n]*\s-m\s*(?:code|pty|pdb)(?![\w.])/i,
168
172
  /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
169
173
  ];
170
174
  const FALLBACK_SURFACE_KEYS = [
@@ -217,6 +221,20 @@ export function formatActionGuardPrompt(toolName, v) {
217
221
  }
218
222
  // --- Audit Logging (local JSONL) ---
219
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; }
220
238
  function writeAuditEntry(entry) {
221
239
  try {
222
240
  mkdirSync(AUDIT_DIR, { recursive: true });
@@ -224,8 +242,9 @@ function writeAuditEntry(entry) {
224
242
  const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
225
243
  appendFileSync(file, JSON.stringify(entry) + '\n');
226
244
  }
227
- catch {
228
- // 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);
229
248
  }
230
249
  }
231
250
  // --- X-Ray Inline Guard ---
@@ -285,9 +304,9 @@ export function createInterceptor(config, pipeline, options) {
285
304
  function guardAuditBase(toolName, v, preview) {
286
305
  return {
287
306
  type: 'intercept', tool: toolName,
288
- severity: v.severity === 'catastrophic' ? 'critical' : 'high',
307
+ severity: v.severity === 'catastrophic' ? 'critical' : v.decision === 'allow' ? 'low' : 'high',
289
308
  firewallResult: 'ACTION_GUARD', threats: v.signals,
290
- anomalyScore: v.decision === 'block' ? 1 : 0.6,
309
+ anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
291
310
  trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
292
311
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
293
312
  };
@@ -331,8 +350,18 @@ export function createInterceptor(config, pipeline, options) {
331
350
  handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
332
351
  return;
333
352
  }
334
- 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
+ }
335
363
  return;
364
+ }
336
365
  const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
337
366
  const base = guardAuditBase(context.toolName, v, preview);
338
367
  const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.6",
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
 
@@ -269,6 +273,9 @@ const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
269
273
  // <interp>` admitted (issue #92 must-fix 3) — mirrors tool-action-guard.ts's
270
274
  // pipe-download-to-shell pattern exactly; kept in sync there.
271
275
  /\b(?:curl|wget|fetch)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:env\s+)?(?:\w+=\S*\s+)*(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?!(?:\s+-[a-z]+)*\s+-[cem]\b)/i,
276
+ // Stdin-executing python MODULES defeat the -m exemption above (issue #86.1) —
277
+ // mirrors tool-action-guard.ts's pipe-download-module-exec; kept in sync there.
278
+ /\b(?:curl|wget|fetch)\b[^|\n]*\|[^\n]*\bpython\d?\b[^\n]*\s-m\s*(?:code|pty|pdb)(?![\w.])/i,
272
279
  /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
273
280
  ];
274
281
 
@@ -324,14 +331,32 @@ export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLik
324
331
 
325
332
  const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
326
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
+
327
351
  function writeAuditEntry(entry: InterceptAuditEntry): void {
328
352
  try {
329
353
  mkdirSync(AUDIT_DIR, { recursive: true });
330
354
  const date = new Date().toISOString().slice(0, 10);
331
355
  const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
332
356
  appendFileSync(file, JSON.stringify(entry) + '\n');
333
- } catch {
334
- // 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);
335
360
  }
336
361
  }
337
362
 
@@ -442,9 +467,9 @@ export function createInterceptor(
442
467
  function guardAuditBase(toolName: string, v: ToolGuardVerdictLike, preview: string): Omit<InterceptAuditEntry, 'action' | 'outcome'> {
443
468
  return {
444
469
  type: 'intercept', tool: toolName,
445
- severity: v.severity === 'catastrophic' ? 'critical' : 'high',
470
+ severity: v.severity === 'catastrophic' ? 'critical' : v.decision === 'allow' ? 'low' : 'high',
446
471
  firewallResult: 'ACTION_GUARD', threats: v.signals,
447
- anomalyScore: v.decision === 'block' ? 1 : 0.6,
472
+ anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
448
473
  trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
449
474
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
450
475
  };
@@ -490,7 +515,18 @@ export function createInterceptor(
490
515
  handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
491
516
  return;
492
517
  }
493
- 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
+ }
494
530
 
495
531
  const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
496
532
  const base = guardAuditBase(context.toolName, v, preview);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.6",
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.6",
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",