@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 +1 -0
- package/dist/interceptor.js +31 -5
- package/dist/openclaw.plugin.json +11 -1
- package/interceptor.ts +40 -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 };
|
|
@@ -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.
|
|
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')
|
|
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);
|
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