@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 +1 -0
- package/dist/interceptor.js +34 -5
- package/dist/openclaw.plugin.json +11 -1
- package/interceptor.ts +43 -7
- package/openclaw.plugin.json +11 -1
- package/package.json +1 -1
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
|
package/dist/interceptor.js
CHANGED
|
@@ -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.
|
|
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')
|
|
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);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
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