@drakon-systems/shieldcortex-realtime 4.42.3 → 4.43.0

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/dist/index.js CHANGED
@@ -675,6 +675,7 @@ export default {
675
675
  enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
676
676
  severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
677
677
  failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
678
+ actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: false }), ...(rawInterceptorConfig.actionGuard ?? {}) },
678
679
  } : {}),
679
680
  logger: { info: api.logger?.info ?? console.log, warn: api.logger?.warn ?? console.warn },
680
681
  };
@@ -692,13 +693,19 @@ export default {
692
693
  if (typeof defenceMod.runDefencePipeline !== 'function')
693
694
  return null;
694
695
  interceptorReady = createInterceptor(interceptorConfig, defenceMod.runDefencePipeline, {
696
+ evaluateToolCall: typeof defenceMod.evaluateToolCall === 'function'
697
+ ? defenceMod.evaluateToolCall
698
+ : undefined,
695
699
  onAuditEntry: (entry) => syncInterceptEvent(entry, {
696
700
  cloudApiKey: scConfig.cloudApiKey ?? '',
697
701
  cloudBaseUrl: scConfig.cloudBaseUrl ?? 'https://api.shieldcortex.ai',
698
702
  cloudEnabled: scConfig.cloudEnabled ?? false,
699
703
  }),
700
704
  });
701
- api.logger?.info?.('[shieldcortex] Interceptor active — watching: remember, mcp__memory__remember');
705
+ const guardState = interceptorConfig.actionGuard?.enabled
706
+ ? (interceptorConfig.actionGuard.enforce ? 'Action Guard: enforce' : 'Action Guard: warn')
707
+ : 'Action Guard: off';
708
+ api.logger?.info?.(`[shieldcortex] Interceptor active — memory writes + ${guardState} (shell/file/network/git)`);
702
709
  return interceptorReady;
703
710
  }
704
711
  catch (err) {
@@ -26,6 +26,12 @@ const DEFAULT_CONFIG = {
26
26
  high: 'deny',
27
27
  critical: 'deny',
28
28
  },
29
+ // Action Guard on by default: catastrophic ops are blocked out of the box;
30
+ // dangerous ops are surfaced (warn+audit) but allowed unless `enforce` is set.
31
+ actionGuard: {
32
+ enabled: true,
33
+ enforce: false,
34
+ },
29
35
  };
30
36
  export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
31
37
  export function extractContent(toolName, args) {
@@ -133,6 +139,33 @@ export function formatApprovalPrompt(input) {
133
139
  '[Approve] [Deny]',
134
140
  ].join('\n');
135
141
  }
142
+ /** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
143
+ export function summariseToolArgs(args) {
144
+ if (!args)
145
+ return '';
146
+ const parts = [];
147
+ for (const [k, val] of Object.entries(args)) {
148
+ if (typeof val === 'string')
149
+ parts.push(`${k}=${val.slice(0, 80)}`);
150
+ else if (typeof val === 'number' || typeof val === 'boolean')
151
+ parts.push(`${k}=${val}`);
152
+ }
153
+ return parts.join(' ').slice(0, 160);
154
+ }
155
+ /** Operator-facing approval prompt for a gated action (not a memory write). */
156
+ export function formatActionGuardPrompt(toolName, v) {
157
+ return [
158
+ '🛡️ ShieldCortex — Action Intercepted',
159
+ '',
160
+ `Tool: ${toolName}`,
161
+ `Action: ${v.action}`,
162
+ `Risk: ${v.severity}`,
163
+ `Signals: ${v.signals.join(', ') || 'none'}`,
164
+ `Reason: ${v.reason}`,
165
+ '',
166
+ '[Approve] [Deny]',
167
+ ].join('\n');
168
+ }
136
169
  // --- Audit Logging (local JSONL) ---
137
170
  const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
138
171
  function writeAuditEntry(entry) {
@@ -194,13 +227,92 @@ export function createInterceptor(config, pipeline, options) {
194
227
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
195
228
  const log = config.logger ?? { info: console.log, warn: console.warn };
196
229
  const onAuditEntry = options?.onAuditEntry;
230
+ const actionGuardCfg = config.actionGuard ?? { enabled: true, enforce: false };
231
+ const evaluateToolCall = options?.evaluateToolCall;
197
232
  function emitAudit(entry) {
198
233
  writeAuditEntry(entry);
199
234
  onAuditEntry?.(entry);
200
235
  }
236
+ function guardAuditBase(toolName, v, preview) {
237
+ return {
238
+ type: 'intercept', tool: toolName,
239
+ severity: v.severity === 'catastrophic' ? 'critical' : 'high',
240
+ firewallResult: 'ACTION_GUARD', threats: v.signals,
241
+ anomalyScore: v.decision === 'block' ? 1 : 0.6,
242
+ trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
243
+ preview: preview.slice(0, 200), ts: new Date().toISOString(),
244
+ };
245
+ }
246
+ // Action Guard: gates non-memory tool calls (shell / file / network / git).
247
+ // This is what makes "Iron Dome protects what the agent DOES" true at runtime.
248
+ async function runActionGuard(context) {
249
+ if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function')
250
+ return;
251
+ let v;
252
+ try {
253
+ v = evaluateToolCall(context.toolName, context.arguments || {});
254
+ }
255
+ catch (err) {
256
+ // A guard error must never break the agent — log and allow. (The memory
257
+ // pipeline is the hard-fail path; the action guard is best-effort.)
258
+ log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
259
+ return;
260
+ }
261
+ if (v.decision === 'allow')
262
+ return;
263
+ const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
264
+ const base = guardAuditBase(context.toolName, v, preview);
265
+ const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
266
+ // Catastrophic / exfil — hard block, always enforced when the guard is enabled.
267
+ if (v.decision === 'block') {
268
+ emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
269
+ throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
270
+ }
271
+ // require_approval — warn-only by default (never nags), prompt when enforcing.
272
+ if (!actionGuardCfg.enforce) {
273
+ log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
274
+ emitAudit({ ...base, action: 'warn', outcome: 'warned' });
275
+ return;
276
+ }
277
+ if (typeof context.requireApproval !== 'function') {
278
+ const failAction = config.failurePolicy[severity];
279
+ emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
280
+ if (failAction === 'deny') {
281
+ throw new Error(`ShieldCortex: tool call blocked — ${v.reason} (no approver, failure policy: deny)`);
282
+ }
283
+ return;
284
+ }
285
+ if (!rateLimiter.shouldAllow()) {
286
+ emitAudit({ ...base, action: 'rate_limit', outcome: 'auto_denied' });
287
+ throw new Error('ShieldCortex: tool call auto-denied (approval rate limit exceeded)');
288
+ }
289
+ let approved;
290
+ try {
291
+ approved = await context.requireApproval(formatActionGuardPrompt(context.toolName, v));
292
+ }
293
+ catch (err) {
294
+ const failAction = config.failurePolicy[severity];
295
+ log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
296
+ emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
297
+ if (failAction === 'deny') {
298
+ throw new Error('ShieldCortex: tool call blocked — approval error, failure policy: deny');
299
+ }
300
+ return;
301
+ }
302
+ if (approved) {
303
+ emitAudit({ ...base, action: 'require_approval', outcome: 'approved' });
304
+ return;
305
+ }
306
+ emitAudit({ ...base, action: 'require_approval', outcome: 'denied' });
307
+ throw new Error('ShieldCortex: tool call denied by user');
308
+ }
201
309
  async function handleToolCall(context) {
202
- if (!WATCHED_TOOLS.includes(context.toolName))
310
+ // Non-memory tools go through the Action Guard (what the agent DOES); the
311
+ // memory-write tools continue through the content defence pipeline below.
312
+ if (!WATCHED_TOOLS.includes(context.toolName)) {
313
+ await runActionGuard(context);
203
314
  return;
315
+ }
204
316
  const { title, content } = extractContent(context.toolName, context.arguments);
205
317
  const fullContent = [title, content].filter(Boolean).join(' ');
206
318
  if (!fullContent.trim())
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.42.3",
3
+ "version": "4.43.0",
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,
package/index.ts CHANGED
@@ -794,6 +794,7 @@ export default {
794
794
  enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
795
795
  severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
796
796
  failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
797
+ actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: false }), ...(rawInterceptorConfig.actionGuard ?? {}) },
797
798
  } : {}),
798
799
  logger: { info: api.logger?.info ?? console.log, warn: (api.logger as any)?.warn ?? console.warn },
799
800
  };
@@ -812,13 +813,19 @@ export default {
812
813
  if (typeof defenceMod.runDefencePipeline !== 'function') return null;
813
814
 
814
815
  interceptorReady = createInterceptor(interceptorConfig, defenceMod.runDefencePipeline as Parameters<typeof createInterceptor>[1], {
816
+ evaluateToolCall: typeof (defenceMod as any).evaluateToolCall === 'function'
817
+ ? ((defenceMod as any).evaluateToolCall as Parameters<typeof createInterceptor>[2] extends { evaluateToolCall?: infer E } ? E : never)
818
+ : undefined,
815
819
  onAuditEntry: (entry) => syncInterceptEvent(entry, {
816
820
  cloudApiKey: (scConfig as any).cloudApiKey ?? '',
817
821
  cloudBaseUrl: (scConfig as any).cloudBaseUrl ?? 'https://api.shieldcortex.ai',
818
822
  cloudEnabled: (scConfig as any).cloudEnabled ?? false,
819
823
  }),
820
824
  });
821
- api.logger?.info?.('[shieldcortex] Interceptor active — watching: remember, mcp__memory__remember');
825
+ const guardState = interceptorConfig.actionGuard?.enabled
826
+ ? (interceptorConfig.actionGuard.enforce ? 'Action Guard: enforce' : 'Action Guard: warn')
827
+ : 'Action Guard: off';
828
+ api.logger?.info?.(`[shieldcortex] Interceptor active — memory writes + ${guardState} (shell/file/network/git)`);
822
829
  return interceptorReady;
823
830
  } catch (err) {
824
831
  (api.logger as any)?.warn?.(`[shieldcortex] Interceptor init failed: ${err instanceof Error ? err.message : err}`);
package/interceptor.ts CHANGED
@@ -7,10 +7,37 @@ export type Severity = 'low' | 'medium' | 'high' | 'critical';
7
7
  export type InterceptAction = 'log' | 'warn' | 'require_approval';
8
8
  export type FailureAction = 'allow' | 'deny';
9
9
 
10
+ /**
11
+ * Action Guard config — gates what the agent DOES (shell/file/network/git),
12
+ * not just what it remembers. Catastrophic operations (rm -rf /, fork bombs,
13
+ * disk wipes, secret exfil) are always blocked when `enabled`; recognised
14
+ * dangerous ops require approval when `enforce` is set, otherwise they are
15
+ * surfaced (warn + audit) but allowed through — so the guard never nags on
16
+ * routine work by default.
17
+ */
18
+ export interface ActionGuardConfig {
19
+ enabled: boolean;
20
+ enforce: boolean;
21
+ }
22
+
23
+ /** Structural shape of a Tool Action Guard verdict (kept local to avoid a
24
+ * compile-time dependency on the main package across the plugin build boundary;
25
+ * the real `evaluateToolCall` from `shieldcortex/defence` is compatible). */
26
+ export interface ToolGuardVerdictLike {
27
+ decision: 'allow' | 'require_approval' | 'block';
28
+ severity: 'benign' | 'sensitive' | 'dangerous' | 'catastrophic' | string;
29
+ family: string;
30
+ action: string;
31
+ reason: string;
32
+ signals: string[];
33
+ }
34
+ export type ToolGuardEvaluator = (toolName: string, args: Record<string, unknown>) => ToolGuardVerdictLike;
35
+
10
36
  export interface InterceptorConfig {
11
37
  enabled: boolean;
12
38
  severityActions: Record<Severity, InterceptAction>;
13
39
  failurePolicy: Record<Severity, FailureAction>;
40
+ actionGuard?: ActionGuardConfig;
14
41
  logger?: { info: (msg: string) => void; warn: (msg: string) => void };
15
42
  }
16
43
 
@@ -63,6 +90,12 @@ const DEFAULT_CONFIG: InterceptorConfig = {
63
90
  high: 'deny',
64
91
  critical: 'deny',
65
92
  },
93
+ // Action Guard on by default: catastrophic ops are blocked out of the box;
94
+ // dangerous ops are surfaced (warn+audit) but allowed unless `enforce` is set.
95
+ actionGuard: {
96
+ enabled: true,
97
+ enforce: false,
98
+ },
66
99
  };
67
100
 
68
101
  export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
@@ -202,6 +235,32 @@ export function formatApprovalPrompt(input: ApprovalPromptInput): string {
202
235
  ].join('\n');
203
236
  }
204
237
 
238
+ /** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
239
+ export function summariseToolArgs(args: Record<string, unknown> | undefined): string {
240
+ if (!args) return '';
241
+ const parts: string[] = [];
242
+ for (const [k, val] of Object.entries(args)) {
243
+ if (typeof val === 'string') parts.push(`${k}=${val.slice(0, 80)}`);
244
+ else if (typeof val === 'number' || typeof val === 'boolean') parts.push(`${k}=${val}`);
245
+ }
246
+ return parts.join(' ').slice(0, 160);
247
+ }
248
+
249
+ /** Operator-facing approval prompt for a gated action (not a memory write). */
250
+ export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLike): string {
251
+ return [
252
+ '🛡️ ShieldCortex — Action Intercepted',
253
+ '',
254
+ `Tool: ${toolName}`,
255
+ `Action: ${v.action}`,
256
+ `Risk: ${v.severity}`,
257
+ `Signals: ${v.signals.join(', ') || 'none'}`,
258
+ `Reason: ${v.reason}`,
259
+ '',
260
+ '[Approve] [Deny]',
261
+ ].join('\n');
262
+ }
263
+
205
264
  // --- Audit Logging (local JSONL) ---
206
265
 
207
266
  const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
@@ -297,6 +356,8 @@ type PipelineRunner = (content: string, title: string, source: { type: string; i
297
356
  interface InterceptorOptions {
298
357
  maxPromptsPerMinute?: number;
299
358
  onAuditEntry?: (entry: InterceptAuditEntry) => void;
359
+ /** Tool Action Guard evaluator, injected from `shieldcortex/defence` at runtime. */
360
+ evaluateToolCall?: ToolGuardEvaluator;
300
361
  }
301
362
 
302
363
  export function createInterceptor(
@@ -311,14 +372,100 @@ export function createInterceptor(
311
372
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
312
373
  const log = config.logger ?? { info: console.log, warn: console.warn };
313
374
  const onAuditEntry = options?.onAuditEntry;
375
+ const actionGuardCfg: ActionGuardConfig = config.actionGuard ?? { enabled: true, enforce: false };
376
+ const evaluateToolCall = options?.evaluateToolCall;
314
377
 
315
378
  function emitAudit(entry: InterceptAuditEntry): void {
316
379
  writeAuditEntry(entry);
317
380
  onAuditEntry?.(entry);
318
381
  }
319
382
 
383
+ function guardAuditBase(toolName: string, v: ToolGuardVerdictLike, preview: string): Omit<InterceptAuditEntry, 'action' | 'outcome'> {
384
+ return {
385
+ type: 'intercept', tool: toolName,
386
+ severity: v.severity === 'catastrophic' ? 'critical' : 'high',
387
+ firewallResult: 'ACTION_GUARD', threats: v.signals,
388
+ anomalyScore: v.decision === 'block' ? 1 : 0.6,
389
+ trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
390
+ preview: preview.slice(0, 200), ts: new Date().toISOString(),
391
+ };
392
+ }
393
+
394
+ // Action Guard: gates non-memory tool calls (shell / file / network / git).
395
+ // This is what makes "Iron Dome protects what the agent DOES" true at runtime.
396
+ async function runActionGuard(context: ToolCallContext): Promise<void> {
397
+ if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function') return;
398
+
399
+ let v: ToolGuardVerdictLike;
400
+ try {
401
+ v = evaluateToolCall(context.toolName, context.arguments || {});
402
+ } catch (err) {
403
+ // A guard error must never break the agent — log and allow. (The memory
404
+ // pipeline is the hard-fail path; the action guard is best-effort.)
405
+ log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
406
+ return;
407
+ }
408
+ if (v.decision === 'allow') return;
409
+
410
+ const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
411
+ const base = guardAuditBase(context.toolName, v, preview);
412
+ const severity: Severity = v.severity === 'catastrophic' ? 'critical' : 'high';
413
+
414
+ // Catastrophic / exfil — hard block, always enforced when the guard is enabled.
415
+ if (v.decision === 'block') {
416
+ emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
417
+ throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
418
+ }
419
+
420
+ // require_approval — warn-only by default (never nags), prompt when enforcing.
421
+ if (!actionGuardCfg.enforce) {
422
+ log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
423
+ emitAudit({ ...base, action: 'warn', outcome: 'warned' });
424
+ return;
425
+ }
426
+
427
+ if (typeof context.requireApproval !== 'function') {
428
+ const failAction = config.failurePolicy[severity];
429
+ emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
430
+ if (failAction === 'deny') {
431
+ throw new Error(`ShieldCortex: tool call blocked — ${v.reason} (no approver, failure policy: deny)`);
432
+ }
433
+ return;
434
+ }
435
+
436
+ if (!rateLimiter.shouldAllow()) {
437
+ emitAudit({ ...base, action: 'rate_limit', outcome: 'auto_denied' });
438
+ throw new Error('ShieldCortex: tool call auto-denied (approval rate limit exceeded)');
439
+ }
440
+
441
+ let approved: boolean;
442
+ try {
443
+ approved = await context.requireApproval(formatActionGuardPrompt(context.toolName, v));
444
+ } catch (err) {
445
+ const failAction = config.failurePolicy[severity];
446
+ log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
447
+ emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
448
+ if (failAction === 'deny') {
449
+ throw new Error('ShieldCortex: tool call blocked — approval error, failure policy: deny');
450
+ }
451
+ return;
452
+ }
453
+
454
+ if (approved) {
455
+ emitAudit({ ...base, action: 'require_approval', outcome: 'approved' });
456
+ return;
457
+ }
458
+ emitAudit({ ...base, action: 'require_approval', outcome: 'denied' });
459
+ throw new Error('ShieldCortex: tool call denied by user');
460
+ }
461
+
320
462
  async function handleToolCall(context: ToolCallContext): Promise<void> {
321
- if (!(WATCHED_TOOLS as readonly string[]).includes(context.toolName)) return;
463
+ // Non-memory tools go through the Action Guard (what the agent DOES); the
464
+ // memory-write tools continue through the content defence pipeline below.
465
+ if (!(WATCHED_TOOLS as readonly string[]).includes(context.toolName)) {
466
+ await runActionGuard(context);
467
+ return;
468
+ }
322
469
 
323
470
  const { title, content } = extractContent(context.toolName, context.arguments);
324
471
  const fullContent = [title, content].filter(Boolean).join(' ');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.42.3",
3
+ "version": "4.43.0",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.42.3",
3
+ "version": "4.43.0",
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",