@drakon-systems/shieldcortex-realtime 4.47.4 → 4.47.6

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
@@ -4,9 +4,9 @@ OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory
4
4
 
5
5
  ## Compatibility
6
6
 
7
- - **Node.js** — ≥ 18 required
7
+ - **Node.js** — ≥ 20 required (the `shieldcortex` peer ships `better-sqlite3` ^12, which needs Node 20+)
8
8
  - **OpenClaw** — ≥ 2026.3.22 required, **≥ 2026.4.23 recommended** — 2026.4.23 added host-package linking for plugins that declare `openclaw` as a peer dependency ([#70462](https://github.com/openclaw/openclaw/pull/70462)), which lets any future `openclaw/plugin-sdk/*` imports resolve without a duplicate runtime bundle
9
- - **ShieldCortex** — ≥ 4.11.1 required
9
+ - **ShieldCortex** — ≥ 4.18.3 required (matches the declared peer dependency; ship both packages at the same version)
10
10
 
11
11
  OpenClaw is declared as an **optional** peer dependency, so installs on older OpenClaw keep working but miss the linking benefit.
12
12
 
@@ -29,9 +29,11 @@ The defensive root `openclaw.plugin.json` is kept for one release on the main pa
29
29
  |------|--------|
30
30
  | `llm_input` | Scans prompts and history through the ShieldCortex defence pipeline. Threats are logged to audit and can forward to ShieldCortex Cloud. |
31
31
  | `llm_output` | Extracts high-signal memories from assistant replies and writes them into ShieldCortex with novelty filtering and dedupe. |
32
- | `before_tool_call` | Runs the Action Guard before tools execute. Catastrophic shell/file/network/git actions are blocked; dangerous actions warn by default or request approval when enforcement is enabled. |
32
+ | `before_tool_call` | Runs the Action Guard before tools execute. Catastrophic shell/file/network/git actions are always blocked. Recognised-dangerous actions are **enforced by default**: attended sessions get an approval prompt, unattended sessions fail closed per `failurePolicy`. Set `actionGuard.enforce: false` to opt down to warn-and-allow, or pre-approve specific operations with `actionGuard.autoApprove`. |
33
+ | `session_end` | Resets the interceptor's per-session caches. |
34
+ | `/shieldcortex-status` | Slash command reporting the plugin's runtime state. |
33
35
 
34
- The plugin is intentionally fire-and-forget: it should not stall the OpenClaw turn loop if ShieldCortex is unavailable.
36
+ The scanning and memory paths are fire-and-forget: they do not stall the OpenClaw turn loop if ShieldCortex is unavailable. The Action Guard is the deliberate exception — it gates tool calls inline, and since 4.47.5 a guard that fails to load falls back to a dependency-free scanner that still denies unambiguous catastrophic operations (fail-closed) rather than allowing everything.
35
37
 
36
38
  ## Installation
37
39
 
@@ -108,6 +110,10 @@ Supported plugin config keys:
108
110
  - `openclawAutoMemoryDedupe`: enable or disable duplicate suppression
109
111
  - `openclawAutoMemoryNoveltyThreshold`: dedupe similarity threshold, `0.6` to `0.99`
110
112
  - `openclawAutoMemoryMaxRecent`: dedupe cache size, `50` to `1000`
113
+ - `actionGuard.enabled`: turn the before-tool-call Action Guard on or off (default `true`)
114
+ - `actionGuard.enforce`: enforce dangerous-operation gating (default `true`); `false` opts down to warn-and-allow. Catastrophic operations are blocked regardless.
115
+ - `actionGuard.autoApprove`: array of operation allowlist entries for unattended agents that legitimately need specific dangerous operations
116
+ - `failurePolicy`: per-severity verdict when a decision can't be obtained unattended (defaults: `low`/`medium` allow, `high`/`critical` deny)
111
117
 
112
118
  ## Auto-memory
113
119
 
package/dist/index.js CHANGED
@@ -798,9 +798,22 @@ export default {
798
798
  const autoMemory = isAutoMemoryEnabled(cfg) ? "on" : "off";
799
799
  const dedupe = isAutoMemoryDedupeEnabled(cfg) ? "on" : "off";
800
800
  const cloud = cfg.cloudApiKey ? "configured" : "not configured";
801
+ // Resolve the Action Guard state the same way initInterceptor() does,
802
+ // so the status line reflects what before_tool_call will actually do.
803
+ const rawInterceptor = cfg.interceptor;
804
+ const guardCfg = {
805
+ ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] }),
806
+ ...(rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.actionGuard ?? {} : {}),
807
+ };
808
+ const interceptorOn = (rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.enabled : undefined) ?? DEFAULT_INTERCEPTOR_CONFIG.enabled;
809
+ const autoApproved = Array.isArray(guardCfg.autoApprove) ? guardCfg.autoApprove.length : 0;
810
+ const guardState = !interceptorOn || !guardCfg.enabled
811
+ ? "off"
812
+ : `${guardCfg.enforce ? "enforce" : "warn"}${autoApproved > 0 ? ` (${autoApproved} auto-approved)` : ""}${interceptorReady ? "" : " — not yet initialised this session"}`;
801
813
  return {
802
814
  text: `ShieldCortex v${_version}\n` +
803
- ` Hooks: llm_input (scan), llm_output (memory)\n` +
815
+ ` Hooks: llm_input (scan), llm_output (memory), before_tool_call (action guard), session_end (cache reset)\n` +
816
+ ` Action guard: ${guardState}\n` +
804
817
  ` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
805
818
  ` Cloud sync: ${cloud}`,
806
819
  };
@@ -143,6 +143,51 @@ export function formatApprovalPrompt(input) {
143
143
  '[Approve] [Deny]',
144
144
  ].join('\n');
145
145
  }
146
+ // --- WS2 fail-closed fallback (guard load/eval failure) ---
147
+ // Deliberately DUPLICATED from tool-action-guard.ts's CATASTROPHIC list, not
148
+ // imported — this file already avoids a compile-time dependency on the main
149
+ // package across the plugin build boundary (see ToolGuardVerdictLike above),
150
+ // and the fallback specifically must keep working when THAT dependency is
151
+ // what's broken. Narrow by design: only the unambiguous, essentially-never-
152
+ // benign catastrophic shapes, so a broken/unwired guard still fails closed on
153
+ // "rm -rf /"-class commands without turning every tool call into a denial
154
+ // whenever the guard is merely unavailable (e.g. mid-upgrade) — that would
155
+ // itself break unattended agents/cron, the outcome ShieldCortex exists to
156
+ // prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
157
+ const FALLBACK_CATASTROPHIC_PATTERNS = [
158
+ /\brm\b[^|;&\n]*?(?:-\w*r\w*f\w*|-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
159
+ /\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
160
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
161
+ /\bmkfs(\.\w+)?\b/i,
162
+ /\bdd\b[^|;&\n]*\bof=\/dev\/(sd|nvme|hd|disk|mmcblk|vd)/i,
163
+ /\b(fdisk|parted|sgdisk|wipefs|blkdiscard)\b/i,
164
+ // Leading `[^\n|]*` (not `[^\n]*`, issue #92 must-fix 1: ReDoS) + `env <assign>
165
+ // <interp>` admitted (issue #92 must-fix 3) — mirrors tool-action-guard.ts's
166
+ // pipe-download-to-shell pattern exactly; kept in sync there.
167
+ /\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,
168
+ /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
169
+ ];
170
+ const FALLBACK_SURFACE_KEYS = [
171
+ 'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
172
+ 'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
173
+ 'url', 'uri', 'endpoint', 'href', 'host', 'to',
174
+ ];
175
+ /** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
176
+ function fallbackExecSurface(args) {
177
+ const parts = [];
178
+ for (const k of FALLBACK_SURFACE_KEYS) {
179
+ const v = args?.[k];
180
+ if (typeof v === 'string' && v.length > 0)
181
+ parts.push(v);
182
+ }
183
+ return parts.join(' ');
184
+ }
185
+ function fallbackCatastrophicMatch(args) {
186
+ const text = fallbackExecSurface(args);
187
+ if (!text)
188
+ return false;
189
+ return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
190
+ }
146
191
  /** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
147
192
  export function summariseToolArgs(args) {
148
193
  if (!args)
@@ -247,19 +292,43 @@ export function createInterceptor(config, pipeline, options) {
247
292
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
248
293
  };
249
294
  }
295
+ // WS2 fail-closed path: when the real guard was never wired in or throws,
296
+ // run the narrow fallback scan and DENY (throw) a catastrophic-looking
297
+ // command instead of silently allowing it. The fallback recognising nothing
298
+ // is not evidence the command is safe, only that it isn't one of the
299
+ // handful of unambiguous shapes — everything else still falls through to
300
+ // the pre-existing fail-OPEN log-and-allow.
301
+ function handleGuardUnavailable(context, reason) {
302
+ if (!fallbackCatastrophicMatch(context.arguments)) {
303
+ log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — allowing ${context.toolName}`);
304
+ return;
305
+ }
306
+ const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
307
+ emitAudit({
308
+ type: 'intercept', tool: context.toolName, severity: 'critical',
309
+ firewallResult: 'ACTION_GUARD_FALLBACK', threats: ['fallback-scan'],
310
+ anomalyScore: 1, trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null,
311
+ pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
312
+ action: 'auto_deny', outcome: 'auto_denied',
313
+ });
314
+ log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
315
+ throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
316
+ }
250
317
  // Action Guard: gates non-memory tool calls (shell / file / network / git).
251
318
  // This is what makes "Iron Dome protects what the agent DOES" true at runtime.
252
319
  async function runActionGuard(context) {
253
- if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function')
320
+ if (!actionGuardCfg.enabled)
321
+ return;
322
+ if (typeof evaluateToolCall !== 'function') {
323
+ handleGuardUnavailable(context, 'evaluateToolCall not wired');
254
324
  return;
325
+ }
255
326
  let v;
256
327
  try {
257
328
  v = evaluateToolCall(context.toolName, context.arguments || {});
258
329
  }
259
330
  catch (err) {
260
- // A guard error must never break the agent — log and allow. (The memory
261
- // pipeline is the hard-fail path; the action guard is best-effort.)
262
- log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
331
+ handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
263
332
  return;
264
333
  }
265
334
  if (v.decision === 'allow')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.4",
3
+ "version": "4.47.6",
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,
@@ -77,6 +77,22 @@
77
77
  "label": "Critical Severity Action",
78
78
  "description": "Action for critical-severity threats (log, warn, require_approval)",
79
79
  "type": "string"
80
+ },
81
+ "interceptor.actionGuard.enabled": {
82
+ "label": "Action Guard",
83
+ "description": "Gate dangerous shell/file/network/git tool calls before they execute. Catastrophic operations are always blocked while enabled.",
84
+ "type": "boolean"
85
+ },
86
+ "interceptor.actionGuard.enforce": {
87
+ "label": "Enforce Action Guard",
88
+ "description": "Enforce dangerous-operation gating (attended: approval prompt; unattended: failurePolicy). Off = warn-and-allow (advisory). Catastrophic operations block regardless.",
89
+ "type": "boolean"
90
+ },
91
+ "interceptor.actionGuard.autoApprove": {
92
+ "label": "Action Guard Auto-approve List",
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
+ "type": "array",
95
+ "advanced": true
80
96
  }
81
97
  },
82
98
  "configSchema": {
@@ -197,6 +213,27 @@
197
213
  "default": "deny"
198
214
  }
199
215
  }
216
+ },
217
+ "actionGuard": {
218
+ "type": "object",
219
+ "additionalProperties": false,
220
+ "properties": {
221
+ "enabled": {
222
+ "type": "boolean",
223
+ "default": true
224
+ },
225
+ "enforce": {
226
+ "type": "boolean",
227
+ "default": true
228
+ },
229
+ "autoApprove": {
230
+ "type": "array",
231
+ "items": {
232
+ "type": "string"
233
+ },
234
+ "default": []
235
+ }
236
+ }
200
237
  }
201
238
  }
202
239
  }
package/index.ts CHANGED
@@ -946,10 +946,23 @@ export default {
946
946
  const autoMemory = isAutoMemoryEnabled(cfg) ? "on" : "off";
947
947
  const dedupe = isAutoMemoryDedupeEnabled(cfg) ? "on" : "off";
948
948
  const cloud = cfg.cloudApiKey ? "configured" : "not configured";
949
+ // Resolve the Action Guard state the same way initInterceptor() does,
950
+ // so the status line reflects what before_tool_call will actually do.
951
+ const rawInterceptor = (cfg as any).interceptor;
952
+ const guardCfg = {
953
+ ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] }),
954
+ ...(rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.actionGuard ?? {} : {}),
955
+ };
956
+ const interceptorOn = (rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.enabled : undefined) ?? DEFAULT_INTERCEPTOR_CONFIG.enabled;
957
+ const autoApproved = Array.isArray(guardCfg.autoApprove) ? guardCfg.autoApprove.length : 0;
958
+ const guardState = !interceptorOn || !guardCfg.enabled
959
+ ? "off"
960
+ : `${guardCfg.enforce ? "enforce" : "warn"}${autoApproved > 0 ? ` (${autoApproved} auto-approved)` : ""}${interceptorReady ? "" : " — not yet initialised this session"}`;
949
961
  return {
950
962
  text:
951
963
  `ShieldCortex v${_version}\n` +
952
- ` Hooks: llm_input (scan), llm_output (memory)\n` +
964
+ ` Hooks: llm_input (scan), llm_output (memory), before_tool_call (action guard), session_end (cache reset)\n` +
965
+ ` Action guard: ${guardState}\n` +
953
966
  ` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
954
967
  ` Cloud sync: ${cloud}`,
955
968
  };
package/interceptor.ts CHANGED
@@ -247,6 +247,53 @@ export function formatApprovalPrompt(input: ApprovalPromptInput): string {
247
247
  ].join('\n');
248
248
  }
249
249
 
250
+ // --- WS2 fail-closed fallback (guard load/eval failure) ---
251
+ // Deliberately DUPLICATED from tool-action-guard.ts's CATASTROPHIC list, not
252
+ // imported — this file already avoids a compile-time dependency on the main
253
+ // package across the plugin build boundary (see ToolGuardVerdictLike above),
254
+ // and the fallback specifically must keep working when THAT dependency is
255
+ // what's broken. Narrow by design: only the unambiguous, essentially-never-
256
+ // benign catastrophic shapes, so a broken/unwired guard still fails closed on
257
+ // "rm -rf /"-class commands without turning every tool call into a denial
258
+ // whenever the guard is merely unavailable (e.g. mid-upgrade) — that would
259
+ // itself break unattended agents/cron, the outcome ShieldCortex exists to
260
+ // prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
261
+ const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
262
+ /\brm\b[^|;&\n]*?(?:-\w*r\w*f\w*|-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
263
+ /\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
264
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
265
+ /\bmkfs(\.\w+)?\b/i,
266
+ /\bdd\b[^|;&\n]*\bof=\/dev\/(sd|nvme|hd|disk|mmcblk|vd)/i,
267
+ /\b(fdisk|parted|sgdisk|wipefs|blkdiscard)\b/i,
268
+ // Leading `[^\n|]*` (not `[^\n]*`, issue #92 must-fix 1: ReDoS) + `env <assign>
269
+ // <interp>` admitted (issue #92 must-fix 3) — mirrors tool-action-guard.ts's
270
+ // pipe-download-to-shell pattern exactly; kept in sync there.
271
+ /\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,
272
+ /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
273
+ ];
274
+
275
+ const FALLBACK_SURFACE_KEYS = [
276
+ 'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
277
+ 'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
278
+ 'url', 'uri', 'endpoint', 'href', 'host', 'to',
279
+ ];
280
+
281
+ /** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
282
+ function fallbackExecSurface(args: Record<string, unknown> | undefined): string {
283
+ const parts: string[] = [];
284
+ for (const k of FALLBACK_SURFACE_KEYS) {
285
+ const v = args?.[k];
286
+ if (typeof v === 'string' && v.length > 0) parts.push(v);
287
+ }
288
+ return parts.join(' ');
289
+ }
290
+
291
+ function fallbackCatastrophicMatch(args: Record<string, unknown> | undefined): boolean {
292
+ const text = fallbackExecSurface(args);
293
+ if (!text) return false;
294
+ return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
295
+ }
296
+
250
297
  /** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
251
298
  export function summariseToolArgs(args: Record<string, unknown> | undefined): string {
252
299
  if (!args) return '';
@@ -403,18 +450,44 @@ export function createInterceptor(
403
450
  };
404
451
  }
405
452
 
453
+ // WS2 fail-closed path: when the real guard was never wired in or throws,
454
+ // run the narrow fallback scan and DENY (throw) a catastrophic-looking
455
+ // command instead of silently allowing it. The fallback recognising nothing
456
+ // is not evidence the command is safe, only that it isn't one of the
457
+ // handful of unambiguous shapes — everything else still falls through to
458
+ // the pre-existing fail-OPEN log-and-allow.
459
+ function handleGuardUnavailable(context: ToolCallContext, reason: string): void {
460
+ if (!fallbackCatastrophicMatch(context.arguments)) {
461
+ log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — allowing ${context.toolName}`);
462
+ return;
463
+ }
464
+ const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
465
+ emitAudit({
466
+ type: 'intercept', tool: context.toolName, severity: 'critical',
467
+ firewallResult: 'ACTION_GUARD_FALLBACK', threats: ['fallback-scan'],
468
+ anomalyScore: 1, trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null,
469
+ pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
470
+ action: 'auto_deny', outcome: 'auto_denied',
471
+ });
472
+ log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
473
+ throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
474
+ }
475
+
406
476
  // Action Guard: gates non-memory tool calls (shell / file / network / git).
407
477
  // This is what makes "Iron Dome protects what the agent DOES" true at runtime.
408
478
  async function runActionGuard(context: ToolCallContext): Promise<void> {
409
- if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function') return;
479
+ if (!actionGuardCfg.enabled) return;
480
+
481
+ if (typeof evaluateToolCall !== 'function') {
482
+ handleGuardUnavailable(context, 'evaluateToolCall not wired');
483
+ return;
484
+ }
410
485
 
411
486
  let v: ToolGuardVerdictLike;
412
487
  try {
413
488
  v = evaluateToolCall(context.toolName, context.arguments || {});
414
489
  } catch (err) {
415
- // A guard error must never break the agent — log and allow. (The memory
416
- // pipeline is the hard-fail path; the action guard is best-effort.)
417
- log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
490
+ handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
418
491
  return;
419
492
  }
420
493
  if (v.decision === 'allow') return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.4",
3
+ "version": "4.47.6",
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,
@@ -77,6 +77,22 @@
77
77
  "label": "Critical Severity Action",
78
78
  "description": "Action for critical-severity threats (log, warn, require_approval)",
79
79
  "type": "string"
80
+ },
81
+ "interceptor.actionGuard.enabled": {
82
+ "label": "Action Guard",
83
+ "description": "Gate dangerous shell/file/network/git tool calls before they execute. Catastrophic operations are always blocked while enabled.",
84
+ "type": "boolean"
85
+ },
86
+ "interceptor.actionGuard.enforce": {
87
+ "label": "Enforce Action Guard",
88
+ "description": "Enforce dangerous-operation gating (attended: approval prompt; unattended: failurePolicy). Off = warn-and-allow (advisory). Catastrophic operations block regardless.",
89
+ "type": "boolean"
90
+ },
91
+ "interceptor.actionGuard.autoApprove": {
92
+ "label": "Action Guard Auto-approve List",
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
+ "type": "array",
95
+ "advanced": true
80
96
  }
81
97
  },
82
98
  "configSchema": {
@@ -197,6 +213,27 @@
197
213
  "default": "deny"
198
214
  }
199
215
  }
216
+ },
217
+ "actionGuard": {
218
+ "type": "object",
219
+ "additionalProperties": false,
220
+ "properties": {
221
+ "enabled": {
222
+ "type": "boolean",
223
+ "default": true
224
+ },
225
+ "enforce": {
226
+ "type": "boolean",
227
+ "default": true
228
+ },
229
+ "autoApprove": {
230
+ "type": "array",
231
+ "items": {
232
+ "type": "string"
233
+ },
234
+ "default": []
235
+ }
236
+ }
200
237
  }
201
238
  }
202
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.4",
3
+ "version": "4.47.6",
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",
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "engines": {
39
- "node": ">=18.0.0",
39
+ "node": ">=20.0.0",
40
40
  "openclaw": ">=2026.4.23"
41
41
  },
42
42
  "publishConfig": {